Documentation Archive Developer
Search

ADC Home > Reference Library > Technical Notes > Legacy Documents > Mac OS 9 & Earlier >

Legacy Documentclose button

Important: This document is part of the Legacy section of the ADC Reference Library. This information should not be used for new development.

Current information on this Reference Library topic can be found here:

Stand-Alone Code, ad nauseam

CONTENTS

This Technical Note discusses many of the issues related to stand-alone code modules. This Note is by no means a completely original work, as the author borrows freely from the work of Keith Rollin, Mark Baumwell, and Jim Friedlander.

[Oct 01 1989]






How to Recognize a Stand-Alone Code When You See One

What Stand-Alone Code Looks Like to the Naked Eye

Stand-alone code is program code which does not enjoy the full status of an application. A stand-alone code module exists as a single Macintosh resource and consists almost entirely of microprocessor-executable object code, and perhaps also some header data and other constants used by the executable portion of the module. Code-type resources are most easily identifiable in the ResEdit 2.0 resource picker. Most of these closely-related resources are indicated by an icon containing a stylized segment of assembly-language source code.

Figure 1. ResEdit 2.0 Icons Signifying Code-Type Resources

Although 'CODE' resources are not stand-alone code modules (they are segments of a larger application), they are similar because they contain executable code and so they have the same code-type icon. Driver resources are a special case of stand-alone code resources, and they have a different icon in the ResEdit picker, reminiscent of the Finder icon for a desk accessory suitcase, because the code of a desk accessory is stored as a 'DRVR' resource. The icon for an 'FKEY' is also a bit different, resembling a function key, naturally.

Table 1 is a fairly extensive list of the currently-defined code-type resources. Many are of interest primarily at a system software level; those stand-alone code resources most commonly created by application-level programmers are noted in boldface. Of course, developers are always free to define new resource types for custom stand-alone modules. 'CUST' is commonly used, as in some of the examples at the end of the discussion.

Table 1 Assorted Code Resource Types

The most common use of stand-alone code is to supplement the standard features provided by the Macintosh Toolbox and operating system. Most of the resource types listed in Table 1 define custom windows, controls, menus, lists, and responses to user input. In this respect, they are slaves to particular Toolbox managers or packages and very often contained within the resource fork of an owner application. Other examples of stand-alone code are more useful as application extensions like HyperCard 'XCMD' and 'XFCN' extensions.

'DRVR', 'INIT', and 'cdev' resources are more autonomous examples of stand-alone code. These allow programmers to write code which may be executed automatically when the system starts up and code which adds special features to the operating system or provides control of special-purpose peripherals and system functions. The temptation here is to perform functions generally reserved for full-blown applications, such as use of QuickDraw. For a number of reasons, this is a non-trivial endeavor, and is the subject of much of this discussion.

How Applications Are Special

Macintosh applications can be almost any size, limited mainly by disk space and RAM size. The actual program code is generally divided up into a number of segments, each less than 32K in size so the amount of memory required to execute a program may be less than the size of the program itself. The Segment Loader, documented in Inside Macintosh, Volume II, controls the loading and unloading of segments. It ensures that the necessary segments of an application are read into the application heap when needed and allows temporarily unneeded sections to be purged, making room for others.

All of this activity occurs in and depends upon a somewhat mysterious construction called an A5 world. It is so called because the A5 register of the microprocessor points indirectly to several key data structures used by the Segment Loader and the application itself. Most Macintosh programmers are at least vaguely aware of the significance of A5 in the Macintosh environment. Many even know that it is a handy pointer to the application and QuickDraw global variables, or at least points in the right general direction. Less widely known is how an A5 world is constructed, and more to the point, how to build one from scratch if necessary.

This may become necessary because higher-level language compilers like MPW Pascal and C automatically generate A5-relative addressing modes to refer to global variables, including QuickDraw globals. The linker then resolves the actual offsets. For example, the ubiquitous

compiles into something equivalent to the following:

    PEA   thePort(A5),-(SP)    ; push a pointer to QuickDraw's thePort variable

Before this is executable, the linker must determine exactly what offset represents thePort. With this value, it patches the object code and creates the code found in the final application. The reader may infer that an application depends on someone else to set up A5 with a meaningful value before program execution begins. This is true, and understanding how this process normally occurs for an application is of paramount importance when writing stand-alone code which needs application-like functionality. Briefly, the Segment Loader allocates space for an A5 world for each application as part of its launch process. Library code is automatically linked to the front of every application, and this sets up A5 to point to the global variable space. The application code begins executing only after all of this preliminary setup is complete.

Figure 2. A Hitchhiker's Guide to the A5 World

How Stand-Alone Code Is Different

Stand-alone code, unlike an application, is never launched. It is simply loaded then executed and possesses no A5 world of its own. Stand-alone code therefore cannot easily define global variables. No space is allocated for globals and A5 either belongs to a real application or is completely meaningless. References to global variables defined by the module usually succeed without even a warning from the linker, but also generally overwrite globals defined by the current application. References to global variables defined in the MPW libraries, like QuickDraw globals, generate fatal link errors.

    Link -t INIT -c '????' -rt INIT=128 -ra =resLocked -m PLAYZOO [[partialdiff]]
        SampleINIT.p.o [[partialdiff]]
        -o SampleINIT
    ### Link: Error: Undefined entry, name: (Error 28) "thePort"
      Referenced from: PLAYZOO in file: SampleINIT.p.o
    ### Link: Errors prevented normal completion.
    ### MPW Shell - Execution of SampleINIT.makeout terminated.

That's not very helpful and not very much fun. So what if a stand-alone code resource needs to use QuickDraw or its associated globals like screenBits? What if a stand-alone module needs to call some "innocuous" routine in the Macintosh Toolbox which implicitly assumes the existence of a valid A5 world? _Unique1ID, which calls the QuickDraw _Random trap, falls into this category, for instance. An 'XCMD' might be able to "borrow" HyperCard's globals, but an 'INIT' has no such alternative; it may need to have its own A5 world.

There are a couple more considerations. Stand-alone code resources are not applications and are not managed by the Segment Loader, so they cannot be segmented into multiple resources like applications. Stand-alone code resources are self-contained modules and are usually less than 32K in size. As popular belief would have it, code resources cannot be more than 32K in size. This is not necessarily true, and although some linkers, especially older ones, enforce the limit all the same, the absolute limitation is that the original Motorola MC68000 microprocessor is not capable of expressing relative offsets which span more than 32K.

A code segment for a 68000-based Macintosh may be any reasonable length, so long as no relative offsets exceed 32K. There are ways to get around this limit even on 68000-based machines, while the MC68020 and later members of the 680x0 family have the ability to specify 32-bit offsets, dissolving the 32K barrier completely as long as the compiler is agreeable. To remain compatible with 68000-based machines, however, and to maintain manageable-sized code segments the 32K "limit" is a good rule of thumb. If a stand-alone code module gets much larger than this, it is often because it's trying to do too much. Remember that stand-alone code should only perform simple and specific tasks.

Writing Your First Stand-Alone Module

Each type of stand-alone code has its own idiosyncrasies. It is difficult to say which type is the easiest to construct. It is best to address each major type individually, but a simple 'INIT' may be the best place to start. Most programmers are pretty familiar with the concept of what an 'INIT' is and how it is used, and its autonomy is a big plus--it is not necessary to write and debug a separate piece of code or a HyperCard stack in which to test the stand-alone module. (This would be necessary for a 'CDEF' or an 'XCMD', for example.) Stand-alone code is often written in assembly language, but high-level languages usually serve just as well. This first example is written in MPW Pascal, in keeping with the precedent set by Inside Macintosh.

SampleINIT is a very simple 'INIT' which plays each of the sounds (resources of type 'snd ') in the System file while the Macintosh boots. It's kind of fun, not too obnoxious, and also not particularly robust. All 'snd ' resources should be unlocked, purgeable, Format 1 sounds like the four default system sounds. Also be sure to name this file SampleINIT.p to work with the SampleINIT.make file which follows. The main subroutine is PlayZoo, in honor of the monkey and dogcow sounds in the author's System file.

    UNIT SampleINIT;    {Pascal stand-alone code is written as a UNIT}

    INTERFACE

      USES
        Types, Resources, Sound;

      {VAR
        cannot define any globals...well, not yet anyway}

      PROCEDURE PlayZoo;

    IMPLEMENTATION

      PROCEDURE PlayZoo;
      VAR
        numSnds, i : INTEGER;
        theSnd : Handle;
        playStatus : OSErr;
      BEGIN
        numSnds := CountResources('snd ');
        FOR i := 1 TO numSnds DO BEGIN
          theSnd := GetIndResource('snd ',i);
          IF theSnd <> NIL THEN
            playStatus := SndPlay(NIL,theSnd,FALSE);
          END;
      END;

Following is the file SampleINIT.make to control the build process:

    #   File:       SampleINIT.make
    #   Target:     SampleINIT
    #   Sources:    SampleINIT.p

    SampleINIT [[florin]][[florin]] SampleINIT.make SampleINIT.p.o
        Link -t INIT -c '????' -rt INIT=128 -ra =resLocked [[partialdiff]]
            -m PLAYZOO
            SampleINIT.p.o [[partialdiff]]
            -o SampleINIT

    SampleINIT.p.o [[florin]] SampleINIT.make SampleINIT.p

That's all there is to it, but even in such a simple example as this, there are a number of extremely important points to highlight. By understanding every nuance of this example, one moves a long way toward understanding everything there is to know about stand-alone code.

Consider first the form of the 'INIT' code itself. It is defined as a UNIT rather than a PROGRAM. This is important, because Pascal programs are applications and require the Segment Loader, preinitialization of A5, and all the things which make an application special. A Pascal unit, like a stand-alone code resource, is simply a collection of subroutines. A similar assembly-language 'INIT' would define and export a PROC. In C, this particular 'INIT' would be a single function in a source file with no main() function.

Pascal programmers may recognize that a unit allows the definition of global variables (as between the USES and PROCEDURE clauses in the INTERFACE section previously documented). Typically, when a unit's object code is linked with a host application, the linker allocates storage for these globals along with the application globals and resolves all necessary A5 references. Stand-alone code modules are never linked with an application, however, and the linker has no way to resolve these references. This makes the linker very unhappy. The easiest way to make the linker happy is to follow the example and define no globals. If globals are really necessary, and they may well be, read on.

Next examine how SampleINIT is linked. To be recognized as a startup document, a "system extension" (as an 'INIT' is called in System 7.0 parlance) must have the file type "INIT". The options -rt and -ra respectively specify that the code resource is of type 'INIT' (ID=128), and that the resource itself is locked. This is a very important idiosyncrasy of the 'INIT' because it is not automatically locked when loaded by the system and might otherwise attempt to move during execution. Hint: this would be very bad.

Finally, PlayZoo is specified as the main entry point by the -m option. When written in Pascal, the entry point of a module is the first compiled instruction. C is a little pickier and demands the main entry point option to override the default entry point (which performs run-time initialization for applications). It is important to remember that the linker does not move the entry point specified by -m to the front of the object file--that is the programmer's responsibility. Specification of this option primarily helps the linker remove dead, unused code from the final object module. In short, don't leave home without it. Note that the linker is case sensitive with respect to identifiers, while the Pascal compiler converts them to all uppercase. It is necessary therefore (in this example) to specify the name of the entry point to the linker in all uppercase characters. If PlayZoo were written in C, which is also case-sensitive, the identifier would be passed to the linker exactly as it appeared in the source code.

For additional examples of stand-alone code, refer to the end of this Note. There are currently a few examples of types of stand-alone code, some of which illustrate the advanced topics discussed in the following sections.

The next area to investigate is how to get around the restrictions on globals in stand-alone code. The first and simplest solution easily conforms to all compatibility guidelines, and that is to avoid using globals altogether. There often comes a time, however, when the use of a global seems unavoidable. The global variable requirements of stand-alone code segments vary, naturally, and there are a number of possible scenarios. Some involve creating an A5 world and others do not. It's best to start with the simplest cases, which do not.

Back to top

Oh, I Have Slipped the Surly Bonds of the Linker . . .

. . . And Have Hung Like a Leach on My Host Application

Often a stand-alone code segment needs the QuickDraw globals of the current application, for whom it is performing a service. A control definition function ('CDEF') is an example. Its drawing operations assume a properly-initialized QuickDraw world, which is graciously provided by the application. Most QuickDraw calls are supported and no special effort is required. One limitation, however, is that explicit references to QuickDraw globals like thePort and screenBits are not allowed. The linker cannot resolve the offsets to these variables because it does not process a 'CDEF' (or any other stand-alone module) along with a particular application. Fortunately the solution is simple, if not entirely straightforward.

Since the structure of the QuickDraw global data is known, as is its location relative to A5, stand-alone code executing as a servant to an application can reference any desired QuickDraw global indirectly. The following code is an example of how a stand-alone unit can make local copies of all the application QuickDraw globals. It uses A5 to locate the variables indirectly, rather than making explicit symbolic references which the linker is not capable of resolving. Figure 2, presented earlier, may be helpful in understanding how this code works.

    UNIT GetQDGlobs;

    INTERFACE

      USES
        Types, QuickDraw, OSUtils;

      TYPE
        QDVarRecPtr  =  ^QDVarRec;
        QDVarRec  =  RECORD
                  randSeed   : Longint;
                  screenBits : BitMap;
                  arrow      : Cursor;
                  dkGray     : Pattern;
                  ltGray     : Pattern;
                  gray       : Pattern;
                  black      : Pattern;
                  white      : Pattern;
                  thePort    : GrafPtr;
                END;

      PROCEDURE GetMyQDVars (VAR qdVars: QDVarRec);

    IMPLEMENTATION

      PROCEDURE GetMyQDVars (VAR qdVars: QDVarRec);
      TYPE
        LongPtr = ^Longint;
      BEGIN
        { Algorithm:
          1.  Get current value of A5 with SetCurrentA5.
          2.  Dereference to get address of thePort.
          3.  Perform arithmetic to determine address of randSeed.
          4.  By assignment, copy the QD globals into a local
              data structure. }
        qdVars := QDVarRecPtr(LongPtr(SetCurrentA5)^ - (SizeOf(QDVarRec)-
                    SizeOf(thePort)))^;
      END;

Extensible Applications

Some applications are intended to be extensible and provide special support for stand-alone code segments. ResEdit for instance, uses 'RSSC' code resources to provide support for custom resource pickers and editors. If a graphical editor is needed to edit a custom resource type, such as an 8 x 64-pixel icon, separately compiled and linked extension code can be pasted directly into the application's resource fork. ResEdit defines interfaces through which it communicates with these resources. In many cases, this degree of support and message passing can preempt the need to declare global variables at all. The ResEdit interfaces are part of the official ResEdit package available from APDA. The MacsBug 'dcmd' is another instance of extension code with support for globals built in. A 'dcmd' specifies in its header how much space it needs for global variables and MacsBug makes room for them.

HyperCard provides high-level support for its 'XCMD' and 'XFCN' extension resources. Callback routines like SetGlobal and GetGlobal provide extension code with a convenient mechanism for defining variables which are global in scope, yet without requiring the deadly A5-relative references normally associated with global variables. The HyperCard interfaces are included with MPW and are called HyperXCmd.p in the Pascal world, or HyperXCmd.h for C programmers.

In cases where an application provides special support for extensions, the extension writer should take advantage of this support as much as possible. Things can get complicated quickly when no support for globals is provided or when built-in support is not used, and there's really no reason to be a masochist. The A5-world techniques described later in this Note usually are not necessary and should be considered extraordinary. Also, when writing an application, it is probably worth considering whether extensibility is useful or desirable. With the move toward object-oriented programming and reusable code, demand for extension module support is growing. Support for extension modules can rarely be tacked on as an afterthought, and it is worth looking at how ResEdit, HyperCard, and Apple File Exchange support modular code when considering similar features. Foresight and planning are indispensable.

Calling Stand-Alone Code from Pascal

Before moving on it may be helpful to look at how one extensible application calls stand-alone code, using HyperCard as an example. The first thing to do is establish some standard means of communication. HyperCard uses a clearly-defined parameter block, as defined in HyperXCmd.p.

    XCmdPtr = ^XCmdBlock;
    XCmdBlock = RECORD
      paramCount:  INTEGER;
      params:      ARRAY [1..16] OF Handle;
      returnValue: Handle;
      passFlag:    BOOLEAN;
      entryPoint:  ProcPtr;    {to call back to HyperCard}
      request:     INTEGER;
      result:      INTEGER;
      inArgs:      ARRAY [1..8] OF LONGINT;
      outArgs:     ARRAY [1..4] OF LONGINT;

An 'XCMD' procedure, like an 'INIT', is written, compiled, and linked as a separate unit. Its prototype may be imagined something like this:

    PROCEDURE MyXCMD (pb: XCMDPtr);

Since MyXCMD is not linked with HyperCard, however, the example declaration does not appear in the HyperCard source code. The prototype only defines how the external module expects to receive its parameters. The host application, HyperCard, is responsible for loading the module and implementing the proper calling conventions.

When calling an 'XCMD', HyperCard first loads the resource into memory and locks it down. It then fills in the parameter block and invokes the 'XCMD'. Notice that the extension module is loaded by its resource name. This is common for extensible applications, since it avoids resource numbering conflicts. Since HyperCard is written in Pascal, the sequence might look something like this.

    theHandle := Get1NamedResource('XCMD', 'MyXCMD');
    HLock(theHandle);
    WITH paramBlock DO
      BEGIN
        { fill it in }
      END;
    CallXCMD(@paramBlock, theHandle);

This also looks a little unwieldy. To fully understand a high-level calling sequence for stand-alone code, a working knowledge of parameter passing conventions and the ability to read code at the assembly-language level is very helpful. Some amount of glue code is almost always required, as illustrated by CallXCMD. After Pascal places a pointer to the parameter block and a handle to the 'XCMD' on the stack, it executes some assembly-language glue represented by three inline opcodes. The glue code finds the 'XCMD' in memory and jumps to it using the handle on the stack. To accomplish this, it pulls the handle off of the stack, dereferences it to obtain a pointer to the 'XCMD' and performs a JSR to the indicated address. The pointer to the parameter block is left on the stack for the 'XCMD'.

    PROCEDURE CallXCMD (pb: XCMDPtr; xcmd: Handle);
      INLINE $205F,    { MOVEA.L  (A7)+,A0         pop handle off stack        }
             $2050,    { MOVEA.L  (A0),A0   dereference to get address of XCMD }
             $4E90;    { JSR      (A0)        call XCMD, leaving pb on stack   }

Figure 3 illustrates the state of the A5 world at four critical phases of the 'XCMD' calling sequence. Boldface indicates approximately where the program counter is pointing, or what code is executing at that moment. The easiest way to read the diagram is to look for features which change from one state to the next. Note in the last state the 'XCMD' knows how to find its parameter block because the stack pointer (A7) initially points to the return address and a pointer to the parameter block is located four bytes above that. If the 'XCMD' is written in a high-level language according to the procedure prototype MyXCMD, as shown above, this procedure is handled automatically by the compiler.

The process is essentially the same when calling stand-alone code from assembly language, but it is not so unnatural. The assembly-language programmer never has to leave their element and generally has a better low-level view of where certain data structures reside and how to access them efficiently. Since the entry point of the stand-alone module can be determined directly, there is no exact parallel to the CallXCMD procedure, and it is not necessary to push a copy of the resource handle on the stack as an intermediate step.

Figure 3. Calling an 'XCMD' from Pascal

Interestingly enough, the CallXCMD procedure can be easily modified to call almost any stand-alone module whose entry point is at the beginning of the code resource. To determine the proper calling interface for a particular code module, simply duplicate the function prototype of the module and add a handle at the end of the argument list. The inline glue does not have to change at all. This works equally well for Pascal procedures or functions, and for any number of arguments including VAR parameters.

Back to top

Doing the A5 Road Trip

There comes a time and place where construction of an A5 world is a "necessary" evil. Usually it's not necessary at all, but sometimes the world really needs just one more Orwellian security 'INIT' to present a dialog at startup. DTS discourages such things, but they happen. Although there is nothing fundamentally or philosophically wrong with constructing a custom A5 world, doing so can create significant technical hassles, and unfortunately, globals make possible a number of user interface atrocities. Generally a different solution, if available, results in simpler and more maintainable code, and reduces the likelihood that your code will go the way of the dinosaur and the passenger pigeon. Furthermore, to make the process of constructing an A5 world as straightforward as possible, yet consistent with normal applications, extensive use is made of two undocumented routines in the MPW run-time libraries. The dangers here are obvious. There are accepted uses, nonetheless. External modules may need to create some global storage or hold data which persists across multiple calls to a routine in the module. All uses shall henceforth be considered fair game, for as it is written in Clarus' memoirs:

Yea, and if It will be done, even in spite,
Then lend Thine hand to the masses,
Lest It be done incorrectly or woefully worse
By those not versed the the ways of the Dogcow.

Who's Got the Map?

The ensuing discussion on how to construct an A5 world is geared primarily to programmers using MPW. There are a couple of reasons for this. First, looking back, the stated problem originated with an error generated by the MPW linker. Other development systems may handle this situation differently and often offer different solutions. Symantec products, for instance, offer A4-relative globals and avoid the globals conflict from the outset. Secondly, this document would resemble a Russian novel if it addressed all the permutations of potential solutions for each development system. MPW Pascal is the de facto standard Macintosh programming environment for illustrative and educational purposes. It may not be fair, but at least it's consistent.

As already described, there are basically three reasons why stand-alone code might need to reserve space for its own global variables. Consider the following three scenarios as a basis, but understand that various arbitrary combinations are also possible:

  • A stand-alone module consists of two functions. There is one main entry point and one function calls another function in the process of calculating its final result. Instead of passing a formal parameter to the subordinate function, the programmer chooses to pass a global.
  • A stand-alone module consists of one function. The module is loaded into memory once and invoked multiple times by the host application. The module requires its own private storage to persist across multiple invocations.
  • A complex 'INIT' uses QuickDraw, or a 'cdev' is complex enough to require an application-like set of globals to accomplish its self-contained task. A module may need to access data in a Toolbox callback (like a dialog hook) where the interface is fixed, for instance.
  • Each of the demonstration units is a working example. There is source code at the end of the discussion for simple applications which can play host to these modules and demonstrate how a complete product fits together.

The first instance is relatively easy to implement. When the module is executed, it creates an A5 world, does its job, and then tears down the A5 world, making sure to restore the host application's world. Such a module may look something like the following example. Pay special attention to the items in boldface. These are specific to the use of globals in stand-alone code.

LazyPass.p

    UNIT LazyPass;

    { This is a stand-alone module which implements the function }
    { of determining a circle's area from its circumference.     }

    INTERFACE

      USES
        Types, SAGlobals;

      FUNCTION CircleArea (circumference: Real) : Real;

    IMPLEMENTATION

      { Define a variable global to all }
      { of the routines in this unit.   }
      VAR radius : Real;

      FUNCTION RadiusSquared : Real;
      FORWARD;

      { CircleArea is defined first so that the entry point is }
      { conveniently located at the beginning of the module.   }

      FUNCTION CircleArea (circumference: Real) : Real;
      VAR
        A5Ref: A5RefType;
        oldA5: Longint;
      BEGIN
        oldA5 := OpenA5World(A5Ref);
          radius := circumference / (2.0 * Pi);
          CircleArea := Pi * RadiusSquared;
        CloseA5World(oldA5, A5Ref);
      END;

      FUNCTION RadiusSquared : Real;
      BEGIN
        RadiusSquared := radius * radius;
      END;

LazyPass.make

This is the makefile for the LazyPass module.

    #   File:       LazyPass.make
    #   Target:     LazyPass
    #   Sources:    LazyPass.p

    OBJECTS = LazyPass.p.o

    LazyPass [[florin]][[florin]] LazyPass.make {OBJECTS}
      Link -w -t '????' -c '????' -rt CUST=128 -m CIRCLEAREA [[partialdiff]]
        -sg LazyPass {OBJECTS} [[partialdiff]]
        "{Libraries}"Runtime.o [[partialdiff]]
        "{Libraries}"Interface.o [[partialdiff]]
        "{PLibraries}"SANELib.o [[partialdiff]]
        "{PLibraries}"PasLib.o [[partialdiff]]
        "{MyLibraries}"SAGlobals.o [[partialdiff]]
        -o LazyPass
    LazyPass.p.o [[florin]] LazyPass.make LazyPass.p

The second instance is a little trickier and requires the cooperation of the host application. The module needs the ability to pass a reference to its global variable storage (A5 world) back to the application so that it can be easily restored the next time the module is invoked. In addition, there must be some way to notify the module the first time and the last time it is to be called. This kind of module is exemplified by the following:

Persist.p

    UNIT Persist;

    { This is a stand-alone module which maintains a running total }
    { of the squares of the parameters it receives. This requires  }
    { the cooperation of a host application. The host must use     }
    { messages to tell the module when to initialize and when to   }
    { tear down. The host also must maintain a handle to the       }
    { module's A5 world between invocations.                       }

    INTERFACE

      USES
        Types, SAGlobals;

      CONST
        kAccumulate = 0;  {These are the control messages.}
        kFirstTime = 1;
        kLastTime = 2;

      FUNCTION AccSquares (parm: Longint; message: Integer;
            VAR A5Ref: A5RefType) : Longint;

    IMPLEMENTATION
      { Define global storage to retain a running }
      { total over multiple calls to the module.  }
      VAR accumulation : Longint;

      FUNCTION AccSquares (parm: Longint; message: Integer;
            VAR A5Ref: A5RefType) : Longint;
      VAR
        oldA5: Longint;
      BEGIN
        IF message = kFirstTime THEN MakeA5World(A5Ref);
        oldA5 := SetA5World(A5Ref);
          IF message = kFirstTime THEN accumulation := 0;
          accumulation := accumulation + (parm * parm);
          AccSquares := accumulation;
        RestoreA5World(oldA5, A5Ref);
        IF message = kLastTime THEN DisposeA5World(A5Ref);
      END;

Persist.make

This is the makefile for the Persist module:

    #   File:       Persist.make
    #   Target:     Persist
    #   Sources:    Persist.p

    OBJECTS = Persist.p.o

    Persist [[florin]][[florin]] Persist.make {OBJECTS}
      Link -w -t '????' -c '????' -rt CUST=129 -m ACCSQUARES [[partialdiff]]
        -sg Persist {OBJECTS} [[partialdiff]]
        "{Libraries}"Runtime.o [[partialdiff]]
        "{Libraries}"Interface.o [[partialdiff]]
        "{PLibraries}"SANELib.o [[partialdiff]]
        "{PLibraries}"PasLib.o [[partialdiff]]
        "{MyLibraries}"SAGlobals.o [[partialdiff]]
        -o Persist
    Persist.p.o [[florin]] Persist.make Persist.p

BigBro; FORWARD;

The third case is illustrated by an 'INIT' using arbitrary Toolbox managers to present a user interface. A working example is too long to present here, but an example is included at the end of the discussion. The process, however, is no more difficult than the examples previously given; there is simply more intervening code to accomplish an interesting task. An 'INIT' may simply call OpenA5World upon entry and CloseA5World before exiting. Everything between can then be just like an application: _InitGraf, _InitWindows, and so on. An 'INIT' should be careful, though, to restore the GrafPort to its initial value before exiting.

Dashing Aside the Curtain, or Revealing the Wizard

Building an A5 world would seem to be fairly complicated, but most of the necessary code is already written. Much of it is in the MPW library called Runtime.o. Actually, this makes sense, since applications have A5 worlds and the programmer doesn't have to do anything special to set them up. Only in the case of stand-alone modules does this become the responsibility of the programmer. What's not in the MPW library is the initial allocation of space for an A5 world. For an application, this is done by the Segment Loader. A stand-alone module can emulate the entire process by using bit of glue code around calls to the appropriate routines in Runtime.o. This is the entire point of the SAGlobals unit. SAGlobals makes it very easy to use globals in stand-alone code because it automates the process of allocating space for globals and initializes them the same way an application would. The Pascal source code for SAGlobals is published here. DTS can also provide the source code in C, as well as simplified Pascal and C headers and the compiled object library.

    { Stand-alone code modules which need to use global variables
      may include the interfaces in this unit. Such code modules
      must also be linked with Runtime.o and SAGlobals.o. }

    UNIT SAGlobals;

    INTERFACE

      USES
        Types, Memory, OSUtils;

      TYPE
        A5RefType = Handle;

      { MakeA5World allocates space for an A5 world based on the
        size of the global variables defined by the module and its
        units. If sufficient space is not available, MakeA5World
        returns NIL for A5Ref and further initialization is aborted. }
      PROCEDURE MakeA5World (VAR A5Ref: A5RefType);

      { SetA5World locks down a previously-allocated handle containing
        an A5 world and sets the A5 register appropriately. The return
        value is the old value of A5 and the client should save it for
        use by RestoreA5World. }
      FUNCTION SetA5World (A5Ref: A5RefType) : Longint;

      { RestoreA5World restores A5 to its original value (which the
        client should have saved) and unlocks the A5 world to avoid
        heap fragmentation in cases where the world is used again. }
      PROCEDURE RestoreA5World (oldA5: Longint; A5Ref: A5RefType);

      { DisposeA5World simply disposes of the A5 world handle. }
      PROCEDURE DisposeA5World (A5Ref: A5RefType);

      { OpenA5World combines MakeA5World and SetA5World for the majority
        of cases in which these two routines are called consecutively. An
        exception is when a single A5 world is invoked many times. In this
        case, the world is only created once with MakeA5World and it is
        invoked each time by SetA5World. Most developers will find it easier
        just to call OpenA5World and CloseA5World at the end. If the memory
        allocation request fails, OpenA5World returns NIL for A5Ref and zero
        in the function result. }
      FUNCTION OpenA5World (VAR A5Ref: A5RefType) : Longint;

      { CloseA5World is the dual of OpenA5World. It combines RestoreA5World
        and DisposeA5World. Again, in certain cases it may be necessary to
        call those two routines explicitly, but most of the time CloseA5World
        is all that is required. }
      PROCEDURE CloseA5World (oldA5: Longint; A5Ref: A5RefType);

    IMPLEMENTATION

      CONST
        kAppParmsSize = 32;

      FUNCTION A5Size : Longint;
        C;  EXTERNAL;    { in MPW's Runtime.o }

      PROCEDURE A5Init (myA5: Ptr);
        C;  EXTERNAL;    { in MPW's Runtime.o }

      PROCEDURE MakeA5World (VAR A5Ref: A5RefType);
      BEGIN
        A5Ref := NewHandle(A5Size);
        { The calling routine must check A5Ref for NIL! }
        IF A5Ref <> NIL THEN
          BEGIN
            HLock(A5Ref);
            A5Init(Ptr(Longint(A5Ref^) + A5Size - kAppParmsSize));
            HUnlock(A5Ref);
          END;
      END;

      FUNCTION SetA5World (A5Ref: A5RefType) : Longint;
      BEGIN
        HLock(A5Ref);
        SetA5World := SetA5(Longint(A5Ref^) + A5Size - kAppParmsSize);
      END;

      PROCEDURE RestoreA5World (oldA5: Longint; A5Ref: A5RefType);
      BEGIN
        IF Boolean (SetA5(oldA5)) THEN;    { side effect only }
        HUnlock(A5Ref);
      END;

      PROCEDURE DisposeA5World (A5Ref: A5RefType);
      BEGIN
        DisposHandle(A5Ref);
      END;

      FUNCTION OpenA5World (VAR A5Ref: A5RefType) : Longint;
      BEGIN
        MakeA5World(A5Ref);
        IF A5Ref <> NIL THEN
          OpenA5World := SetA5World(A5Ref)
        ELSE
          OpenA5World := 0;
      END;

      PROCEDURE CloseA5World (oldA5: Longint; A5Ref: A5RefType);
      BEGIN
        RestoreA5World(oldA5, A5Ref);
        DisposeA5World(A5Ref);
      END;

It is tempting to reduce the entire globals issue to this cookbook recipe. The preceding examples may tend to reinforce this view, but a solid theoretical understanding may be indispensable depending on what sort of code goes between MakeA5World and DisposeA5World. In the Sorter example at the end of this discussion, for instance, an 'XCMD' makes callbacks to HyperCard. There is a similar mechanism between Apple File Exchange and custom translators. When making these callbacks, it is necessary to temporarily restore the host's A5 world. Otherwise, the host application bombs when it finds a different set of variables referenced by A5. Calling SetA5 before and after a callback solves the problem, but neither the problem nor the solution is exactly part of the SAGlobals recipe. Hence, if a programmer chooses to use the SAGlobals unit without understanding how and why it works, they most likely get in a lot of trouble and end up writing to Apple to ask why it doesn't work right. As the best mathematics and physics students generally attest: don't just memorize formulas--know the concepts behind them.

A5Size and A5Init are the MPW library routines necessary to set up and initialize an A5 world. A5Size determines how much memory is required for the A5 world. This memory consists of two parts: memory for globals and memory for application parameters. A5Init takes a pointer to the A5 globals and initializes them to the appropriate values. How this works needs a little explaining.

When MPW links an application together, it has to describe what the globals area should look like. At the very least, it needs to keep track of how large the globals section should be. In addition, it may need to specify what values to put into the globals area. Normally, this means setting everything to zero, but some languages like C allow specification of preinitialized globals. The linker normally creates a packed data block that describes all of this and places it into a segment called %A5Init. Also included in this segment are the routines called by the MPW run-time initialization package to act upon this data. A5Size and A5Init are two such routines. A5Size looks at the field that holds the unpacked size of the data and returns it to the caller. A5Init is responsible for unpacking the data into the globals section. In the case of a stand-alone module, all code and data needs to be packed into a single segment or resource, so %A5Init is not used. The linker option -sg is used to make sure that everything is in the same resource. The MPW Commando interface to CreateMake is very good about specifying this automatically, but the programmer must remember to specify this if they create their own makefiles.

The rest of the SAGlobals unit is mostly self-explanatory. The Memory Manager calls straightforwardly allocate the amount of space indicated by A5Size, and lock the handle down when in use by the module. If the math performed by MakeA5World and SetA5World seems just a little too cosmic in nature, don't be alarmed. It's really quite simple. Referring back to Figure 2, A5 needs to point to the boundary between the global variables and the application parameters. Since the application parameters, including the pointer to QuickDraw globals, are 32 bytes long, the formula should become clear. It's just starting address + block length - 32.

As demonstrated in the examples, a module can simply call MakeA5World to begin building its own A5 world, and it can call SetA5World to invoke it and make it active. What is not demonstrated particularly well in the examples is that the module should check A5Ref to see if it is NIL. If so, there is not space to allocate the A5 world, and the module needs to abort gracefully or find another way of getting its job done. Also, the programmer should be aware that A5Ref is not an actual A5 value. It is a reference to an A5 world as its name implies. The actual value of A5 is calculated whenever that world is invoked, as described in the preceding paragraph.

Back to top

Are We There Yet?

As the preceding sections indicate, stand-alone code is one of the more esoteric areas of Macintosh programming. Many more pages could be devoted to the subject, and they probably will be eventually, but there should be enough information here to get most developers past the initial hurdles of creating stand-alone modules and interfacing with an environment biased toward full-blown applications. As always, suggestions for additional topics are welcome and will be incorporated as demand requires and resources permit.

Party on, Dudes.

Back to top

LazyTest

LazyTest.p

This is a very simple program to demonstrate use of the LazyPass module documented earlier. Things to watch out for are standard I/O (ReadLn and WriteLn) and error checking (or lack thereof). This is a bare-bones example of how to load and call a stand-alone module. Don't expect anything more.

    PROGRAM LazyTest;
    USES
      Types, Resources, Memory, OSUtils;

    VAR
      a, c: Real;
      h1: Handle;

      FUNCTION CallModule (parm: Real; modHandle: Handle) : Real;
      INLINE $205F,    { MOVEA.L  (A7)+,A0       pop handle off stack        }
             $2050,    { MOVEA.L  (A0),A0 dereference to get address of XCMD }
             $4E90;    { JSR      (A0)      call XCMD, leaving pb on stack   }

    BEGIN
      Write('Circumference:');
      ReadLn(c);

      h1 := GetResource('CUST',128);
      HLock(h1);
      a := CallModule(c,h1);
      HUnlock(h1);

      WriteLn('Area: ',a);

LazyTest.make

The accompanying makefile is pretty basic, the kind of thing one expects from CreateMake. The only notable addition is a directive to include the LazyPass module in the final application. This avoids the need to paste LazyPass into the application manually with ResEdit. It is also an example of a very powerful feature of the MPW scripting language, which allows the output of one command to be "piped" into the input of another.

    #   File:       LazyTest.make
    #   Target:     LazyTest
    #   Sources:    LazyTest.p

    OBJECTS = LazyTest.p.o

    LazyTest [[florin]][[florin]] LazyTest.make LazyPass
      Echo 'Include "LazyPass";' | Rez -o LazyTest

    LazyTest [[florin]][[florin]] LazyTest.make {OBJECTS}
      Link -w -t APPL -c '????' [[partialdiff]]
        {OBJECTS} [[partialdiff]]
        "{Libraries}"Runtime.o [[partialdiff]]
        "{Libraries}"Interface.o [[partialdiff]]
        "{PLibraries}"SANELib.o [[partialdiff]]
        "{PLibraries}"PasLib.o [[partialdiff]]
        -o LazyTest
    LazyTest.p.o [[florin]] LazyTest.make LazyTest.p

Back to top

PersistTest

PersistTest.p

PersistTest is an equally minimal application to demonstrate the Persist module, also documented earlier.

    PROGRAM PersistTest;

    USES
      Types, Resources, Memory, OSUtils;

    CONST
      N = 5;
      kAccumulate = 0;    {These are the control messages.}
      kFirstTime = 1;
      kLastTime = 2;

    VAR
      i : Integer;
      acc : Longint;
      h1, otherA5: Handle;

      FUNCTION CallModule (parm: Longint; message: Integer; VAR otherA5: Handle;
        modHandle: Handle) : Longint;
      INLINE $205F,    { MOVEA.L  (A7)+,A0         pop handle off stack        }
             $2050,    { MOVEA.L  (A0),A0   dereference to get address of XCMD }
             $4E90;    { JSR      (A0)        call XCMD, leaving pb on stack   }

    BEGIN
      h1 := GetResource('CUST',129);
      MoveHHi(h1);
      HLock(h1);

      FOR i := 1 TO N DO
        BEGIN
          CASE i OF
            1: acc := CallModule(i,kFirstTime,otherA5,h1);
            N: acc := CallModule(i,kLastTime,otherA5,h1);
          OTHERWISE
            acc := CallModule(i,kAccumulate,otherA5,h1);
          END;
          WriteLn('SumSquares after ',i,' = ',acc);
        END;
      HUnlock(h1);

PersistTest.make

This makefile presents nothing new and is provided for the sake of completeness.

    #   File:       PersistTest.make
    #   Target:     PersistTest
    #   Sources:    PersistTest.p

    OBJECTS = PersistTest.p.o

    PersistTest [[florin]][[florin]] PersistTest.make Persist
      Echo 'Include "Persist";' | Rez -o PersistTest

    PersistTest [[florin]][[florin]] PersistTest.make {OBJECTS}
      Link -w -t APPL -c '????' [[partialdiff]]
        {OBJECTS} [[partialdiff]]
        "{Libraries}"Runtime.o [[partialdiff]]
        "{Libraries}"Interface.o [[partialdiff]]
        "{PLibraries}"SANELib.o [[partialdiff]]
        "{PLibraries}"PasLib.o [[partialdiff]]
        -o PersistTest
    PersistTest.p.o [[florin]] PersistTest.make PersistTest.p

Back to top

Sorter

Sorter.p

Sorter is an example 'XCMD' which demonstrates the concept of persistent globals across multiple invocations. It also illustrates how stand-alone modules must handle callbacks to a host application. This is evidenced by the SetA5 instructions bracketing HyperCard callback routines, such as ZeroToPas, SetGlobal, or user routines incorporating such calls.

{$Z+} { This allows the Linker to find "ENTRYPOINT" without our having to
      put it in the INTERFACE section }

UNIT Fred;

  INTERFACE

    USES
      Types, Memory, OSUtils, HyperXCmd, SAGlobals;

  IMPLEMENTATION

    TYPE
      LongArray = ARRAY [0..0] OF Longint; { These define our list of entries }
      LongPointer = ^LongArray;
      LongHandle = ^LongPointer;

    CONST
      kFirstTime = 1;     { being called for the first time. Initialize. }
      kLastTime = 2;      { being called for the last time. Clean up. }
      kAddEntry = 3;      { being called to add an entry to our list to sort. }
      kSortEntries = 4;   { being called to sort and display our list. }
      kCommandIndex = 1;  { Parameter 1 holds our command number. }
      kA5RefIndex = 2;    { Parameter 2 holds our A5 world reference. }
      kEntryIndex = 3;    { Parameter 3 holds a number to add to our list. }

    VAR
      gHostA5: Longint; { The saved value of our host's (HyperCard's) A5. }
      gNumOfEntries: Longint; { The number of entries in our list. }
      gEntries: LongHandle; { Our list of entries. Gets expanded as needed. }

      { Forward reference to the main procedure. This is so we can jump to
      it from ENTRYPOINT, which represents the beginning of the XCMD, and is
      what HyperCard calls when it calls us. }

    PROCEDURE Sorter(paramPtr: XCmdPtr);
      FORWARD;

    PROCEDURE ENTRYPOINT(paramPtr: XCmdPtr);

      BEGIN
        Sorter(paramPtr);
      END;

    { Utility routines for using the HyperCard callbacks. There are some
      functions that we need to perform many times, or would like to
      encapsulate into little routines for clarity:

        ValueOfExpression - given an index from 1 to 16, this evaluates the
            expression of that parameter. This is used to scoop out the value
            of the command selector, our A5 pointer, and the value of the
            number we are to stick into our list of numbers to sort.

        LongToZero - Convert a LONGINT into a C (zero delimited) string.
            Returns a handle that contains that string.

        SetGlobalAt - given the index to one of the 16 parameters and a
            LONGINT, this routines sets the global found in that parameter to
            the LONGINT.
    }

    FUNCTION ValueOfExpression(paramPtr: XCmdPtr;
                               index: integer): Longint;

      VAR
        tempStr: Str255;
        tempHandle: Handle;

      BEGIN
        ZeroToPas(paramPtr, paramPtr^.params[index]^, tempStr);
        tempHandle := EvalExpr(paramPtr, tempStr);
        ZeroToPas(paramPtr, tempHandle^, tempStr);
        DisposHandle(tempHandle);
        ValueOfExpression := StrToLong(paramPtr, tempStr);
      END;

    FUNCTION LongToZero(paramPtr: XCmdPtr;
                        long: Longint): Handle;

      VAR
        tempStr: Str255;

      BEGIN
        LongToStr(paramPtr, long, tempStr);
        LongToZero := PasToZero(paramPtr, tempStr);
      END;

    PROCEDURE SetGlobalAt(paramPtr: XCmdPtr;
                          index: integer;
                          long: Longint);

      VAR
        globalName: Str255;
        hLong: Handle;

      BEGIN
        ZeroToPas(paramPtr, paramPtr^.params[index]^, globalName);
        hLong := LongToZero(paramPtr, long);
        SetGlobal(paramPtr, globalName, hLong);
        DisposHandle(hLong);
      END;

    { These 4 routines are called according to the command passed to the XCMD:

        Initialize - used to initialize our globals area. A5Init will clear
            our globals to zero, and set up any pre-initialized variables if we
            wrote our program in C or Assembly, but it can't do everything. For
            instance, in this XCMD, we need to create a handle to hold our list
            of entries.
        AddAnEntry - Takes the value represented by the 3 parameters passed to
            us by HyperCard and adds it to our list.
        SortEntries - Sorts the entries we have so far. Converts them into a
            string and tells HyperCard to display them in the message box.
        FreeData - We just receive the message saying that we are never going
            to be called again. Therefore, we must get rid of any memory we
            have allocated for our own use.
    }

    PROCEDURE Initialize;

      BEGIN
        gEntries := LongHandle(NewHandle(0));
        gNumOfEntries := 0;
      END;

    PROCEDURE AddAnEntry(paramPtr: XCmdPtr);

      VAR
        ourA5: Longint;
        tempStr: Str255;
        temp: Longint;

      BEGIN
        ourA5 := SetA5(gHostA5);
        temp := ValueOfExpression(paramPtr, kEntryIndex);
        ourA5 := SetA5(ourA5);

        SetHandleSize(Handle(gEntries), (gNumOfEntries + 1) * 4);
        {$PUSH} {$R-}
        gEntries^^[gNumOfEntries] := temp;
        {$POP}
        gNumOfEntries := gNumOfEntries + 1;
      END;

    PROCEDURE SortEntries(paramPtr: XCmdPtr);

      VAR
        ourA5: Longint;
        i, j: integer;
        fullStr: Str255;
        tempStr: Str255;
        temp: Longint;

      BEGIN
        IF gNumOfEntries > 1 THEN
          BEGIN
          {$PUSH} {$R-}
          FOR i := 0 TO gNumOfEntries - 2 DO
            BEGIN
            FOR j := i + 1 TO gNumOfEntries - 1 DO
              BEGIN
              IF gEntries^^[i] > gEntries^^[j] THEN
                BEGIN
                temp := gEntries^^[i];
                gEntries^^[i] := gEntries^^[j];
                gEntries^^[j] := temp;
                END;
              END;
            END;
          {$POP}
          END;

        IF gNumOfEntries > 0 THEN
          BEGIN
          fullStr := '';
          FOR i := 0 TO gNumOfEntries - 1 DO
            BEGIN
            {$PUSH} {$R-}
            temp := gEntries^^[i];
            {$POP}
            ourA5 := SetA5(gHostA5);
            NumToStr(paramPtr, temp, tempStr);
            ourA5 := SetA5(ourA5);
            fullStr := concat(fullStr, ', ', tempStr);
            END;
          delete(fullStr, 1, 2); { remove the first ", " }
          ourA5 := SetA5(gHostA5);
          SendHCMessage(paramPtr, concat('put "', fullStr, '"'));
          ourA5 := SetA5(ourA5);
          END;
      END;

    PROCEDURE FreeData;

      BEGIN
        DisposHandle(Handle(gEntries));
      END;

    { Main routine. Big Cheese. Head Honcho. The Boss. The Man with all the
      moves. You get the idea. This is the controlling routine. It first
      checks to see if we have the correct number of parameters (sort of).
      If that's OK, then it either creates a new A5 world and initializes it,
      or it sets up one that we've previously created.  It then dispatches to
      the appropriate routine, depending on what command was passed to us.
      Finally, it restores the host application's A5 world, and disposes of
      ours if this is the last time we are being called. }

    PROCEDURE Sorter(paramPtr: XCmdPtr);

      VAR
        command: integer;
        A5Ref: A5RefType;
        errStr: Str255;
        A5Name: Str255;

      BEGIN {Main}

        WITH paramPtr^ DO
          IF (paramCount < 2) OR (paramCount > 3) THEN
            BEGIN
            errStr :=
 'Correct usage is: "Sorter <function> <A5> [<entry>]"';
            paramPtr^.returnValue := PasToZero(paramPtr, errStr);
            EXIT(Sorter); {leave the XCMD}
            END;

        command := ValueOfExpression(paramPtr, kCommandIndex);

        IF command = kFirstTime THEN
          BEGIN
          MakeA5World(A5Ref);
          SetGlobalAt(paramPtr, kA5RefIndex, Longint(A5Ref));
          END
        ELSE
          BEGIN
          A5Ref := A5RefType(ValueOfExpression(paramPtr, kA5RefIndex));
          END;

        IF (A5Ref = NIL) THEN
          BEGIN
          errStr := 'Could not get an A5 World!!!';
          paramPtr^.returnValue := PasToZero(paramPtr, errStr);
          EXIT(Sorter); {leave the XCMD}
          END;

        gHostA5 := SetA5World(A5Ref);
        CASE command OF
          kFirstTime: Initialize;
          kAddEntry: AddAnEntry(paramPtr);
          kSortEntries: SortEntries(paramPtr);
          kLastTime: FreeData;
        END;
        RestoreA5World(gHostA5, A5Ref);

        IF command = kLastTime THEN DisposeA5World(A5Ref)

      END; {main}


Sorter.make

The makefile for Sorter is fairly straightforward, but CreateMake cannot generate all of it automatically. Be sure to link with both HyperXLib.o and SAGlobals.o, and account for any custom directories to search for interfaces. In most of the examples, there are two MPW Shell variables, MyInterfaces and MyLibraries which represent the directories containing the SAGlobals headers and library, respectively. Someone following along with these examples would need to define these Shell variables, possibly in the UserStartup file, or replace the occurrences with the name of whatever directory actually contains the necessary SAGlobals files.

#   File:       Sorter.make
#   Target:     Sorter
#   Sources:    Sorter.p

OBJECTS = Sorter.p.o

Sorter [[florin]][[florin]] Sorter.make {OBJECTS}
  Link -w -t '????' -c '????' -rt XCMD=256 -m ENTRYPOINT [[partialdiff]]
    -sg Sorter {OBJECTS} [[partialdiff]]
    "{Libraries}"Runtime.o [[partialdiff]]
    "{Libraries}"Interface.o [[partialdiff]]
    "{PLibraries}"SANELib.o [[partialdiff]]
    "{PLibraries}"PasLib.o [[partialdiff]]
    "{Libraries}"HyperXLib.o [[partialdiff]]
    "{MyLibraries}"SAGlobals.o [[partialdiff]]
    -o Sorter
Sorter.p.o [[florin]] Sorter.make Sorter.p

A Sample HyperCard Script Using Sorter

To test Sorter, it is necessary to create a simple HyperCard stack. After creating a new stack under HyperCard's File menu, use the button tool to create a new button and associate it with the following script. Now use ResEdit to paste the 'XCMD' resource "Sorter" into the stack and it's ready for experimentation.

on mouseUp
  global A5
  Sorter 1, "A5"       -- Initialize that puppy
  if the result is empty then
    Sorter 3, A5, 6    -- Add some numbers to the list
    Sorter 3, A5, 2
    Sorter 3, A5, 9
    Sorter 3, A5, 12
    Sorter 3, A5, 7
    Sorter 4, A5       -- sort them and print them
    Sorter 2, A5       -- Dispose of our data
  else
    put the result
  end if

Back to top

BigBro

BigBro.p

BigBro may look a bit familiar because it performs the same function as the sample INIT offered early in the preceding discussion. However, it has the added feature of providing a user interface, or a dialog at least, during the startup sequence. This tends to make it very obnoxious, and DTS discourages this sort of thing on human interface grounds. Nonetheless, it is an interesting case study. It is also the first example in which a stand-alone code resource uses other resources.

UNIT BigBro;

INTERFACE

  USES
    Types, SAGlobals, OSUtils,
    QuickDraw, Fonts, Windows, Menus, TextEdit, Dialogs,
    Resources, Sound, ToolUtils;
  PROCEDURE BeAPest;

IMPLEMENTATION

  PROCEDURE BeAPest;
  CONST
    kBigBroDLOG = 128;
  VAR
    A5Ref: A5RefType;
    oldA5: Longint;
    numSnds, i, itemHit: Integer;
    theSnd: Handle;
    playStatus: OSErr;
    orwell: DialogPtr;

  BEGIN
    IF NOT Button THEN BEGIN
      oldA5 := OpenA5World(A5Ref);
      IF A5Ref <> NIL THEN BEGIN
        InitGraf(@thePort);
        InitFonts;
        InitWindows;
        InitMenus;
        TEInit;
        InitDialogs(NIL);
        InitCursor;
        orwell := GetNewDialog(kBigBroDLOG, NIL, WindowPtr(-1));
        numSnds := CountResources('snd ');
        FOR i := 1 TO numSnds DO BEGIN
          theSnd := GetIndResource('snd ',i);
          IF theSnd <> NIL THEN
            playStatus := SndPlay(NIL,theSnd,FALSE);
          END;
        REPEAT
          ModalDialog(NIL, itemHit);
        UNTIL itemHit = 1;
        DisposDialog(orwell);
      CloseA5World(oldA5, A5Ref);
      END;
    END;
  END;

BigBro.r

This is the Rez input file necessary to create the 'DLOG' and 'DITL' resources used by BigBro.

resource 'DLOG' (128) {
  {84, 124, 192, 388},
  dBoxProc,
  visible,
  noGoAway,
  0x0,
  128,
  ""
};

resource 'DITL' (128) {
  {  /* array DITLarray: 2 elements */
    /* [1] */
    {72, 55, 93, 207},
    Button {
      enabled,
      "Continue Booting"
    },
    /* [2] */
    {13, 30, 63, 237},
    StaticText {
      disabled,
      "This is an exaggerated case of the type "
      "of INIT which bothers me more than anyth"
      "ing else."
    }
  }

BigBro.make

The makefile for BigBro is a little simpler than that of Sorter, but includes an extra directive to include the dialog resources using Rez. Refer to the Sorter example for notes on the MyInterfaces and MyLibraries Shell variables.

#   File:       BigBro.make
#   Target:     BigBro
#   Sources:    BigBro.p


OBJECTS = BigBro.p.o

BigBro [[florin]][[florin]] BigBro.make BigBro.r
  Rez -o BigBro "{RIncludes}"Types.r BigBro.r


BigBro [[florin]][[florin]] BigBro.make {OBJECTS}
  Link -w -t INIT -c '????' -rt INIT=128 -ra =resLocked [[partialdiff]]
    -m BEAPEST -sg BigBro {OBJECTS} [[partialdiff]]
    "{Libraries}"Runtime.o [[partialdiff]]
    "{Libraries}"Interface.o [[partialdiff]]
    "{PLibraries}"SANELib.o [[partialdiff]]
    "{PLibraries}"PasLib.o [[partialdiff]]
    "{MyLibraries}"SAGlobals.o [[partialdiff]]
    -o BigBro
BigBro.p.o [[florin]] BigBro.make BigBro.p

Back to top

MyWindowDef

MyWindowDef.a

Writing a 'WDEF' is like writing an 'INIT', except that 'WDEF' resources have standard headers that are incorporated into the code. In this example, the 'WDEF' is the Pascal MyWindowDef. To create the header, use an assembly language stub:

    StdWDEF    MAIN EXPORT             ; this will be the entry point
               IMPORT MyWindowDef      ; name of Pascal FUNCTION that is the WDEF
                                       ; we IMPORT externally referenced routines
                                       ; from Pascal (in this case, just this one)
               BRA.S    @0             ; branch around the header to the actual code
               DC.W     0               ; flags word
               DC.B     'WDEF'          ; type
               DC.W     3               ; ID number
               DC.W     0               ; version
    @0         JMP      MyWindowDef     ; this calls the Pascal WDEF

MyWindowDef.p

Now for the Pascal source for the 'WDEF'. Only the shell of what needs to be done is listed, the actual code is left as an exercise for the reader (for further information about writing a 'WDEF', see Inside Macintosh, Volume I, The Window Manager (pp. 297-302) and Volume V, The Window Manager (pp. 205-206).

    UNIT WDef;

    INTERFACE

      USES MemTypes, QuickDraw, OSIntf, ToolIntf;

    {this is the only external routine}
      FUNCTION MyWindowDef(varCode: Integer; theWindow: WindowPtr;
            message: Integer;
            param: LongInt): LongInt; {As defined in IM p. I-299}

    IMPLEMENTATION

      FUNCTION MyWindowDef(varCode: Integer; theWindow: WindowPtr;
            message: Integer;
            param: LongInt): LongInt;

        TYPE
          RectPtr = ^Rect;

        VAR
          aRectPtr : RectPtr;

      {here are the routines that are dispatched to by MyWindowDef}

        PROCEDURE DoDraw(theWind: WindowPtr; DrawParam: LongInt);
          BEGIN {DoDraw}
            {Fill in the code!}
          END; {DoDraw}

        FUNCTION DoHit(theWind: WindowPtr; theParam: LongInt): LongInt;
          BEGIN {DoHit}
            {Code for this FUNCTION goes here}
          END;  {DoHit}

        PROCEDURE DoCalcRgns(theWind: WindowPtr);
          BEGIN {DoCalcRgns}
            {Code for this PROCEDURE goes here}
          END;  {DoCalcRgns}

        PROCEDURE DoGrow(theWind: WindowPtr; theGrowRect: Rect);
          BEGIN {DoGrow}
            {Code for this PROCEDURE goes here}
          END;  {DoGrow}

        PROCEDURE DoDrawSize(theWind: WindowPtr);
          BEGIN {DoDrawSize}
            {Code for this PROCEDURE goes here}
          END;  {DoDrawSize}

      {now for the main body to MyWindowDef}
      BEGIN  { MyWindowDef }
      {case out on the message and jump to the appropriate routine}
        MyWindowDef := 0; {initialize the function result}

        CASE message OF
          wDraw:  { draw window frame}
            DoDraw(theWindow,param);

          wHit:   { tell what region the mouse was pressed in}
            MyWindowDef := DoHit(theWindow,param);

          wCalcRgns: { calculate structRgn and contRgn}
            DoCalcRgns(theWindow);

          wNew:   { do any additional initialization}
            { we don't need to do any}
            ;

          wDispose:{ do any additional disposal actions}
            { we don't need to do any}
            ;

          wGrow:  { draw window's grow image}
            BEGIN
              aRectPtr := RectPtr(param);
              DoGrow(theWindow,aRectPtr^);
            END; {CASE wGrow}

          wDrawGIcon:{ draw Size box in content region}
            DoDrawSize(theWindow);

        END; {CASE}
      END; {MyWindowDef}

MyWindowDef.make (Pascal Version)

    #   File:       MyWindowDef.make
    #   Target:     MyWindowDef
    #   Sources:    MyWindowDef.a MyWindowDef.p

    OBJECTS = MyWindowDef.a.o MyWindowDef.p.o

    MyWindowDef [[florin]][[florin]] MyWindowDef.make {OBJECTS}
      Link -w -t '????' -c '????' -rt WDEF=3 -m STDWDEF [[partialdiff]]
        -sg MyWindowDef {OBJECTS} [[partialdiff]]
        -o MyWindowDef
    MyWindowDef.a.o [[florin]] MyWindowDef.make MyWindowDef.a
       Asm  MyWindowDef.a
    MyWindowDef.p.o [[florin]] MyWindowDef.make MyWindowDef.p

That's all there is to it.

MyWindowDef.c

Writing a 'WDEF' in MPW C is very similar to writing one in Pascal. You can use the same assembly language header, and all you need to make sure of is that the main dispatch routine (in this case: MyWindowDef) is first in your source file. Here's the same 'WDEF' shell in MPW C:

    /* first, the mandatory includes */
    #include <types.h>
    #include <quickdraw.h>
    #include <resources.h>
    #include <fonts.h>
    #include <windows.h>
    #include <menus.h>
    #include <textedit.h>
    #include <events.h>

    /* declarations */
    void DoDrawSize();
    void DoGrow();
    void DoCalcRgns();
    long int DoHit();
    void DoDraw();

    /*---------------------- Main Proc within WDEF ------------------*/
    pascal long int MyWindowDef(varCode,theWindow,message,param)
    short int    varCode;
    WindowPtr    theWindow;
    short int    message;
    long int     param;

    {    /* MyWindowDef */

      Rect       *aRectPtr;
      long int   theResult=0;               /*this is what the function
                                                  returns, init to 0 */

      switch (message)
      {
        case wDraw:                              /* draw window frame*/
          DoDraw(theWindow,param);
          break;
        case wHit:       /* tell what region the mouse was pressed in*/
          theResult = DoHit(theWindow,param);
          break;
        case wCalcRgns:            /* calculate structRgn and contRgn*/
          DoCalcRgns(theWindow);
          break;
        case wNew:                /* do any additional initialization*/
          break;                                     /* nothing here */
        case wDispose:          /* do any additional disposal actions*/
          break;                           /* we don't need to do any*/
        case wGrow:                       /* draw window's grow image*/
          aRectPtr = (Rect *)param;
          DoGrow(theWindow,*aRectPtr);
          break;
        case wDrawGIcon:           /* draw Size box in content region*/
          DoDrawSize(theWindow);
          break;
      }  /* switch */
      return theResult;
    }    /* MyWindowDef */

    /* here are the routines that are dispatched to by MyWindowDef

    /*--------------------------- DoDraw function --------------------*/
    void DoDraw(WindToDraw,DrawParam)
    WindowPtr    WindToDraw;
    long int     DrawParam;

    {  /* DoDraw */
          /* code for DoDraw goes here */
    }  /* DoDraw */

    /*--------------------------- DoHit function ---------------------*/
    long int DoHit(WindToTest,theParam)
    WindowPtr    WindToTest;
    long int     theParam;

    {  /* DoHit */
          /* code for DoHit goes here */
    }  /* DoHit */

    /*------------------------ DoCalcRgns procedure -------------------*/
    void DoCalcRgns(WindToCalc)
    WindowPtr    WindToCalc;

    {  /* DoCalcRgns */
          /* code for DoCalcRgns goes here */
    }  /* DoCalcRgns */

    /*-------------------------- DoGrow procedure ---------------------*/
    void DoGrow(WindToGrow,theGrowRect)
    WindowPtr    WindToGrow;
    Rect         theGrowRect;

    {  /* DoGrow */
          /* code for DoGrow goes here */
    }  /* DoGrow */

    /*------------------------ DoDrawSize procedure -------------------*/
    void DoDrawSize(WindToDraw)
    WindowPtr    WindToDraw;

    {  /* DoDrawSize */
          /* code for DoDrawSize goes here */

MyWindowDef.make (C Version)

    #   File:       MyWindowDef.make
    #   Target:     MyWindowDef
    #   Sources:    MyWindowDef.a MyWindowDef.c

    OBJECTS = MyWindowDef.a.o MyWindowDef.c.o

    MyWindowDef [[florin]][[florin]] MyWindowDef.make {OBJECTS}
      Link -w -t '????' -c '????' -rt WDEF=3 [[partialdiff]]
        -m STDWDEF -sg MyWindowDef {OBJECTS} [[partialdiff]]
        -o MyWindowDef
    MyWindowDef.a.o [[florin]] MyWindowDef.make MyWindowDef.a
       Asm  MyWindowDef.a
    MyWindowDef.c.o [[florin]] MyWindowDef.make MyWindowDef.c

Back to top

Debugger 'FKEY'

DebugKey.a

DebugKey a very simple assembly-language example of how to write an 'FKEY' code resource, which traps to the debugger. With this 'FKEY', you can enter the debugger using the keyboard rather than pressing the interrupt switch on your Macintosh.

The build process is a little different for this example, as it links the 'FKEY' directly into the System file. Another script can remove the 'FKEY' resource. If the prospect of turning MPW tools loose on the System file is just too much to bear, the 'FKEY' can be linked into a separate file and pasted into the System file with a more mainstream tool like ResEdit.

    ; File: DebugKey.a
    ;
    ; An FKEY to invoke the debugger via command-shift-8
    ;
    DebugKey    MAIN

                BRA.S    CallDB    ;Invoke the debugger
                ;standard header
                DC.W     $0000     ;flags
                DC.L     'FKEY'    ;'FKEY' is 464B4559 hex
                DC.W     $0008     ;FKEY Number
                DC.W     $0000     ;Version number
    CallDB      DC.W     $A9FF     ;Debugger trap
                RTS

InstallDBFKEY (An MPW Installation Script)

    #  DebugKey Installer Script
    #
    #  Place this file in the current directory and type
    #  "InstallDBFKEY <Enter>" to install the debugger FKEY
    #  in your System file.
    #
      Asm DebugKey.a


Back to top

References

Inside Macintosh, Volumes I & V, The Window Manager

Inside Macintosh, Volume II, The Memory Manager & The Segment Loader

Inside Macintosh, Volume V, The Start Manager

MPW Reference Manual

Technical Note #208, Setting and Restoring A5

Technical Note #240, Using MPW for Non-Macintosh 68000 Systems

Back to top

Downloadables

Acrobat gif

Acrobat version of this Note (K).

Download