Writing computer programs that work properly is hard. Programmers, being bright, but fundamentally lazy people, are always on the lookout for ways to shortcut the process. Sometimes they are successful—the move away from assembly language towards using high-level languages for most programming tasks, which was largely complete by the 1980s, brought about an order-of-magnitude improvement in the productivity of programmers, and in the quality of the code they produced.
But things have largely stood still since then. Many ideas for bringing about further improvements in programmer productivity and code quality were being floated about in the 1980s, but history shows that none of them has worked. That also goes for the only one of those ideas that most people still remember, and that some are still trying to promote: object-oriented programming.
Anybody who doubts this analysis need not take my word for it: pick up a copy of The Mythical Man Month, Essays on Software Engineering, Anniversary Edition by Frederick P Brooks Jr (Addison-Wesley 1995, ISBN 0-201-83595-9). This book is a classic compilation of experiences and thoughts on managing large software projects. It was already becoming clear, when this 1995 edition was being prepared, that object orientation was failing to fulfil any of the promises that had been made for it—that it would improve code reuse, reduce bugs, or make for easier or faster development. Seven years on, nothing has changed: none of those things has come about.
So, it looks like all the easy answers have been found: if we are to make any further progress, it’s going to take hard work. Which means developing good programming habits, and more than a touch of discipline.
Good habits? Discipline? But programmers are creative types, aren’t they? So won’t you put off your potential readership by using such unfashionable words? (So some might ask.)
To which I reply: I’m only interested in those readers who are willing to learn new things. If you’re put off by the thought that it takes work to achieve improvements, then you might as well stop reading now.
What’s the single biggest complaint users have with their software today? That it tends to crash or behave strangely, for no good reason. What’s the single most likely cause of this? It’s code that doesn’t deal properly with error conditions, or that doesn’t manage memory properly.
The two issues—errors and memory—tend to go together. On the one hand, a common error condition is trying to allocate some memory and being unable to because you don’t have enough. On the other hand, correctly freeing up memory when you are finished with it can become complicated when you didn’t actually succeed in doing what you were trying to do (because of an error condition), so you now have to recover gracefully and free up the memory you had allocated for the failed operation—bearing in mind that you might not have succeeded in allocating all the needed memory in the first place.
Both these issues have been long-standing facts of life in computer programming. Some bright minds have sought to do away with, or at least simplify, them in ingenious ways: by introducing exceptions to deal with error conditions, and garbage collection to automatically deal with freeing up unused memory.
Let’s deal with the second one first.
Garbage collection, as a concept, is about as old as the concept of high-level languages—both date from around the 1950s. Both concepts were initially resisted, on the grounds that they imposed too much overhead on the software that was developed with them. As machines became faster and cheaper, and programmer time became more expensive, this argument slowly faded away in the case of high-level languages, but it still remains in the case of garbage collection. Here we are, a half-century later, with machines about 5 orders of magnitude faster than those early, expensive behemoths now in commonplace use on all our desktops, and garbage collection still remains stymied by the efficiency issue.
It is worth noting why this is the case. Both techniques involve giving up some efficiency in the generated code for the sake of ease of programming and greater reliability of the resulting software. But whereas the technological improvements over the last half-century have made this tradeoff even more compelling in the case of high-level languages (at least, those that don’t depend on garbage collection for their implementation), they have made the relative overhead of garbage collection even worse.
This is because not all the parts of a modern computer are improving at the same rate. The parts which are already the fastest—such as the CPU—continue to gain speed at a greater rate than the parts which are most in need of improvement—such as main memory and hard disks. This has been the trend for so many decades now that it seems unlikely to change in the foreseeable future.
To get around this disparity between the speeds of its different parts, computers make more and more use of caches to hold frequently-used data at various levels of the storage hierarchy, to minimize the need to fetch that data from the next lower (and slower) level. Thus, current machines are already up to two, and some are getting on to three levels of caches between the CPU and main memory. Hard disk controllers have caches to try to reduce the requirement to actually transfer data between the disk and main memory. And so it goes.
The key to the effectiveness of all these caches is the principle of locality: this means that the majority of the accesses that a running program makes to a particular level of storage (main memory, hard disk) are done to only a small fraction of that storage. So if you can hold just that small fraction of the storage in a fast cache, you can get most of the speedup that you might achieve if the cache were big enough to replace the lower-level storage entirely.
So, the fact that we do indeed observe our current machines to be faster than older ones is a testament to how good the principle of locality is, as an approximation to the behaviour of real programs. Most of them, that is, with the particular exception of garbage-collected ones.
The trouble with garbage collection is that it involves scavenging memory blocks that have fallen out of use. Since the blocks aren’t currently in use, it is also extremely likely that they are no longer resident in the memory caches. So the garbage-collection process has to keep bringing unused blocks back into the memory caches in order to mark them for reuse. Clearly, garbage collection violates the principle of locality—it is a cache-hostile activity. Which is why it continues to be too expensive for most real-world uses, even on today’s superfast machines.
The other concept, that of using exceptions as a means of achieving radical transfers of control when unusual situations arise in the program, is a little bit newer—it seems to date from the 1970s. Already by the 1980s, there had arisen a number of different approaches to implementing exceptions, because it had become apparent that they were a little too prone to getting out of control in real-world use. Thus, we see the rule introduced that a routine must declare all the exceptions that it could potentially raise, to try to avoid things getting completely out of hand. But then, this starts to complicate the code a lot, particularly when that routine calls other routines that can in their turn raise a whole bunch of new exceptions, that the calling routine must also declare in its exceptions list if it doesn’t trap them.
So then we have the rule in C++, where it becomes optional to declare the exceptions you can raise, but if you do declare them, you must declare all of them. (So how is that supposed to help?) Or Java, where you only need to declare your exceptions if they are subclasses of Exception, but not if, for example, they are subclasses of Error.
When people start introducing complications upon complications like this in order to fix up an idea, it’s a strong indication that the basic idea is flawed and should be abandoned. And so it is with exceptions: it is well-known that they can get out of hand, leading to the code doing things it wasn’t supposed to do (which is precisely what you’re trying to prevent with robust error handling). But the only known mechanisms for keeping them under control add too much complication to the code, so people decide to make these mechanisms optional. Which means they’re avoiding solving the real problems with exceptions. So why bother?
Another concept that originated in the 1970s was that of structured programming—the idea that, by following some basic structuring rules in the writing of your code, you could make it a lot less buggy and a lot easier to maintain.
The structured-programming movement was steeped in controversy from the start. One thing that got people particularly worked up was the idea that the goto-statement should be banned. This seemed to be going too far, particularly when the goto was so useful for recovering from exceptional error conditions. Hit a problem in a sequence of code that means that the rest of that sequence can proceed? Simple—just put in a goto which bypasses the rest of that sequence. Much simpler than putting in a whole lot of structures within structures, each one of which has to check for the error condition and propagate it back out, right?
Unfortunately, that’s not how most real-world code works. That sequence of code that hit the error probably also allocated some memory blocks, which need to be correctly freed before passing on the error condition. Thus, you cannot avoid setting up structures within structures, if you want to deal correctly with memory allocations in the presence of errors.
Once you realize this, you also realize that all those objections to the banning of the goto go away—there really is no place for it in well-written code. This may sound like an extreme viewpoint, but speaking as an experienced programmer, the only times I can recall needing a goto in the last 20 years were when using languages that didn’t support adequate control constructs (most of which, thankfully, are extinct now). I have never written a goto in C code.
Another useful characteristic of structures is that they nest—you can put them inside one another, and take them out again. I often rewrite my code, and I assume other programmers do the same. A sequence inside one routine later turns out to be useful in other places, so it becomes worth taking it out and making it a new routine by itself, called from the original place. Or sometimes I go the other way—something was initially separated out later turns out not to be worth keeping separate. If your code is using gotos, this rearrangement turns out to be unpleasantly like trying to disentangle clumps of cold, wet spaghetti—hence the term spaghetti code.
So what is a programming structure? The key part about it is that it is always entered from the top and exited from the bottom. Consider the following routine:
Boolean BlockEqual
(
const void * Src1,
const void * Src2,
UInt32 Count
)
/* compares two memory blocks, returning true iff they
are byte-for-byte equal. */
{
for (;;)
{
if (Count == 0)
return
true;
if (*(uchar *)Src1 != *(uchar *)Src2)
return
false;
++*(ulong *)&Src1;
++*(ulong *)&Src2;
--Count;
} /*for*/
} /*BlockEqual*/
This violates the structuring rule, because the two return-statements don’t just exit the for-statement, they also exit the routine. To see this, consider that if you put a statement following the for-statement, it would never be reached. Thus, the for-statement is not being exited out the bottom.
Here’s how I would write the above:
Boolean BlockEqual
(
const void * Src1,
const void * Src2,
UInt32 Count
)
/* compares two memory blocks, returning true iff they
are byte-for-byte equal. */
{
Boolean Result;
for (;;)
{
if (Count == 0)
{
Result = true;
break;
} /*if*/
if (*(uchar *)Src1 != *(uchar *)Src2)
{
Result = false;
break;
} /*if*/
++*(ulong *)&Src1;
++*(ulong *)&Src2;
--Count;
} /*for*/
return
Result;
} /*BlockEqual*/
Note that, while a structure typically consists of a single control construct at the top level, not every control construct is a structure. For example, each of the if-statements in the above routine is not a structure, because they contain break-statements that prevent control from being transferred to the following statements—thus, they are not being exited at the bottom. This is fine, because each if-statement doesn’t do much that is very exciting on its own: it is the overall for-statement, together with suitable accompanying definitions of the entities referenced in it, that is the useful unit.
Another reason for organizing your code into structures has to do with maximizing the use of your brain cells. Simplifying outrageously, let’s divide the brain cells that you use for programming problems into Type A and Type B. Type A brain cells are the ones for higher-level functions, such as understanding what the code is doing, while Type B cells deal with lower-level tasks, like checking that your code is correctly indented. You have only so many brain cells of each type, and Type A cells are in particularly short supply. So why waste them on tasks that can be just as easily handled by Type B cells? This is what structuring helps you to do, by producing repeatable, recognizable patterns in your code.
Smart programmers don’t necessarily have more Type A brain cells than ordinary programmers; they just know how to use them more effectively.
So how exactly do we use structures to ease the job of coping with errors while managing memory correctly?
My technique is to use a four-stage arrangement to the code, the stages being, in sequence, Initialize, Allocate, Process and Dispose:
The first important point is that stages 1 and 4 cannot fail. The only errors can occur in stages 2 and 3. The second important point is that, once stage 1 has been entered, and regardless of any errors in stages 2 and 3, stage 4 will always be executed. And the third important point is that, if there are any errors in stage 2, then stage 3 will be skipped.
For example, consider the following routine for returning a copy, at a specified pointer address, of the data on the MacOS Clipboard (having previously determined its size):
static void GetInboundScrapData
(
OSType DataType,
void * Dest,
OSErr * Err
)
/* copies the data of the specified type from the scrap to the
specified destination. */
{
Handle TempHandle;
SInt32 ScrapStatus, ScrapOffset;
/* Initialize */
TempHandle = nil;
do /*once*/
{
/* Allocate */
TempHandle = NewHandle(0);
*Err = MemError();
if (*Err != noErr)
break;
/* Process */
ScrapStatus = GetScrap
(
/*hDest =*/ TempHandle,
/*theType =*/ DataType,
/*offset =*/ &ScrapOffset
);
if (ScrapStatus < 0)
{
*Err = ScrapStatus;
break;
} /*if*/
BlockMoveData
(
/*sourcePtr =*/ *TempHandle,
/*destPtr =*/ Dest,
/*byteCount =*/ ScrapStatus
);
}
while (false);
/* Dispose */
DisposeHandleVar(&TempHandle);
} /*GetInboundScrapData*/
Note how easy it is to convince yourself of the following, just by reading the code:
This is actually quite an achievement! It is normally extremely difficult to test code to ensure that you have fully exercised all the possible error-recovery paths: how much easier it is if you can have a high level of confidence in the robustness of your error-handling just from reading the code!
Besides the four stages as marked, there are some additional features in the above code which you probably find unusual, which you will be seeing a lot more of later:
DisposeHandleVar is a very simple routine from my standard libraries, defined as follows:
void DisposeHandleVar
(
Handle * TheHandle
)
/* disposes of a handle if non-nil, and sets it to nil. */
{
if (*TheHandle != nil)
{
DisposeHandle(*TheHandle);
*TheHandle = nil;
} /*if*/
} /*DisposeHandleVar*/
As mentioned, this is an idempotent routine, which means it does no harm to call it more than once on the same handle variable, nor is there any harm in calling it if the memory was never allocated in the first place.
Note also that the disposal routine does not check for any errors. I could certainly put in a check (for example, an attempt to dispose of an invalid non-nil handle), but any such error would indicate a bug in the program. Thus, there is no point in returning such an error indication to the caller; the best I could do would be to put up an immediate fatal error alert, and force the program to abort. This could be a useful thing to do, as a technique for flushing out bugs: I just haven’t bothered in this case.
These are the general requirements I impose on all my disposal routines: they must be idempotent, and never return any errors. If the underlying system calls do not fulfil these requirements, then I define wrappers around them (such as the one above) which do.
The second interesting feature of the above example routine is the do-once construct. This conforms to the rule for structures which nest: you enter it at the top, and exit it at the bottom. It allows for a linear sequence of any number of steps, any of which can fail, in which all prior steps must have succeeded in order for a given step to be processed.
Oddly, some people who see this construct for the first time tend to react unfavourably. It is a loop construct which is not being used for looping, and a few people don’t like that. What can I say? It is a deficiency of the C language that it doesn’t offer a construct specifically for this purpose, so reusing the do-construct (and carefully marking it as such right from the beginning, with the /*once*/ comment) is the best I can do. I need a structure for this purpose, and the alternatives that have been suggested to me are even worse. Deal with it!
Here I’ll be looking at various pieces of published sample code, and showing you how to improve them using the above ideas.
To start with, looking at the following routine (taken from the file DTSQTUtilities.c at <ftp://ftp.apple.com/developer/Sample_Code/QuickTime/Basics/DragAndDrop_Shell.sit>):
pascal OSErr QTUPlayMovieSound(Movie theMovie)
{
OSErr anErr = noErr;
Handle tempHandle = NewHandle(1);
DebugAssert(theMovie != NULL); if(theMovie == NULL) return invalidMovie;
// Extract first sound track.
anErr = PutMovieIntoTypedHandle(theMovie, (Track)0, 'snd ', tempHandle, 0, GetMovieDuration(theMovie),
0, (ComponentInstance)0); DebugAssert(anErr == noErr);
if(anErr != noErr) goto Closure;
anErr = MemError(); DebugAssert(anErr == noErr);
if(anErr != noErr) goto Closure;
// Play sound resource async.
anErr = SndPlay(0L, (SndListHandle)tempHandle, true); DebugAssert(anErr == noErr);
Closure:
if(tempHandle) DisposeHandle(tempHandle);
return anErr;
}
This code uses two different methods for aborting on an error: a return-statement and a whole bunch of “goto Closure” statements. Can you spot the bug arising from this inconsistency? It is that, if theMovie is passed in as nil, the routine still allocates tempHandle, but forgets to deallocate it! Here’s how I would rewrite it, with the four stages as marked:
pascal OSErr QTUPlayMovieSound
(
Movie theMovie
)
{
OSErr anErr;
Handle tempHandle;
/* Initialize */
anErr = noErr;
tempHandle = nil;
do /*once*/
{
DebugAssert(theMovie != nil);
if (theMovie == nil)
{
anErr = invalidMovie;
break;
} /*if*/
/* Allocate */
tempHandle = NewHandle(1);
anErr = MemError();
DebugAssert(anErr == noErr);
if (anErr != noErr)
break;
/* Process */
/* Extract first sound track. */
anErr = PutMovieIntoTypedHandle
(
/*theMovie =*/ theMovie,
/*targetTrack =*/ nil,
/*handleType =*/ 'snd ',
/*publicMovie =*/ tempHandle,
/*start =*/ 0,
/*dur =*/ GetMovieDuration(theMovie),
/*flags =*/ 0,
/*userComp =*/ nil
);
DebugAssert(anErr == noErr);
if (anErr != noErr)
break;
anErr = MemError();
DebugAssert(anErr == noErr);
if (anErr != noErr)
break;
/* Play sound resource async. */
anErr = SndPlay
(
/*chan =*/ nil,
/*sndHandle =*/ (SndListHandle)tempHandle,
/*async =*/ true
);
DebugAssert(anErr == noErr);
}
while (false);
/* Dispose */
DisposeHandleVar(&tempHandle);
return
anErr;
} /*QTUPlayMovieSound*/
Note here that there is some additional code between the Initialize and Allocate stages, namely a check that the movie being passed in is not nil. This is fine; the check can skip the Allocate and Process stages, but it has no effect on whether the Dispose stage is executed. Thus, the important points I stated above about the behaviour of the four stages are not violated.
Here’s another example from the same source file:
pascal SeqGrabComponent QTUCreateSequenceGrabber(WindowPtr theWindow)
{
OSErr anErr = noErr;
SeqGrabComponent s = NULL;
DebugAssert(theWindow != NULL); if(theWindow == NULL) goto Closure;
s = OpenDefaultComponent(SeqGrabComponentType, 0);
if(s) // we got a valid one
{
anErr = SGInitialize(s); DebugAssert(anErr == noErr);
if(anErr != noErr) goto Closure;
anErr = SGSetGWorld(s, (CGrafPtr)theWindow, NULL); DebugAssert(anErr == noErr);
if(anErr != noErr) goto Closure;
}
return s;
Closure:
return NULL;
}
Can you spot the problem with this one? It is that, if there is an error doing the setup of the sequence grabber component instance, it is not returned as the function result, but neither is it disposed!
Here’s an initial rewrite:
pascal SeqGrabComponent QTUCreateSequenceGrabber
(
WindowPtr theWindow
)
{
OSErr anErr;
SeqGrabComponent s;
anErr = noErr;
s = nil;
do /*once*/
{
DebugAssert(theWindow != nil);
if (theWindow == nil)
break;
s = OpenDefaultComponent(SeqGrabComponentType, 0);
if (s == nil)
break;
anErr = SGInitialize(s);
DebugAssert(anErr == noErr);
if (anErr != noErr)
break;
anErr = SGSetGWorld
(
/*s =*/ s,
/*gp =*/ (CGrafPtr)theWindow,
/*gd =*/ nil
);
DebugAssert(anErr == noErr);
if (anErr != noErr)
break;
}
while (false);
if (anErr != noErr)
{
CloseComponentVar(&s);
} /*if*/
return
s;
} /*QTUCreateSequenceGrabber*/
Note the idempotent CloseComponentVar routine. Its definition is
void CloseComponentVar
(
ComponentInstance * Instance
)
/* closes a component instance if non-nil, and sets it to nil. */
{
if (*Instance != nil)
{
(void)CloseComponent(*Instance);
*Instance = nil;
} /*if*/
} /*CloseComponentVar*/
These idempotent disposal routines are small, but they are used so frequently, that it really does save both typing time and program source size to have them.
But the trouble with this example is that it doesn’t return an error code, so you don’t really have any clue as to why it might have failed. So here’s a more commercial-strength version of the code:
void QTUCreateSequenceGrabber
(
WindowPtr TheWindow,
SeqGrabComponent * TheGrabber,
OSErr * Err
)
{
*Err = noErr;
*TheGrabber = nil; /* to begin with */
do /*once*/
{
DebugAssert(TheWindow != nil);
if (TheWindow == nil)
{
*Err = paramErr;
break;
} /*if*/
*Err = OpenADefaultComponent
(
/*componentType =*/ SeqGrabComponentType,
/*componentSubType =*/ 0,
/*ci =*/ TheGrabber
);
if (*Err != noErr)
break;
*Err = SGInitialize(*TheGrabber);
DebugAssert(*Err == noErr);
if (*Err != noErr)
break;
*Err = SGSetGWorld
(
/*s =*/ *TheGrabber,
/*gp =*/ (CGrafPtr)TheWindow,
/*gd =*/ nil
);
DebugAssert(*Err == noErr);
if (*Err != noErr)
break;
}
while (false);
if (*Err != noErr)
{
CloseComponentVar(TheGrabber);
} /*if*/
} /*QTUCreateSequenceGrabber*/
An important point about this structuring technique is that it doesn’t just work with simple examples: it also scales gracefully up to more complex situations. For instance, consider this longer example from <ftp://ftp.apple.com/developer/Sample_Code/QuickTime/Wired_Movies_and_Sprites/AddHTActions.sit>:
static OSErr AddHTAct_AddHyperTextToTextMovie (FSSpec *theFSSpec)
{
short myResID = 0;
short myResRefNum = -1;
Movie myMovie = NULL;
Track myTrack = NULL;
Media myMedia = NULL;
TimeValue myTrackOffset;
TimeValue myMediaTime;
TimeValue mySampleDuration;
TimeValue mySelectionDuration;
TimeValue myNewMediaTime;
TextDescriptionHandle myTextDesc = NULL;
Handle mySample = NULL;
short mySampleFlags;
Fixed myTrackEditRate;
QTAtomContainer myActions = NULL;
OSErr myErr = noErr;
//////////
//
// open the movie file and get the first text track from the movie
//
//////////
// open the movie file for reading and writing
myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm);
if (myErr != noErr)
goto bail;
myErr = NewMovieFromFile(&myMovie, myResRefNum, &myResID, NULL, newMovieActive, NULL);
if (myErr != noErr)
goto bail;
// find first text track in the movie
myTrack = GetMovieIndTrackType(myMovie, kIndexOne, TextMediaType, movieTrackMediaType);
if (myTrack == NULL)
goto bail;
//////////
//
// get first media sample in the text track
//
//////////
myMedia = GetTrackMedia(myTrack);
if (myMedia == NULL)
goto bail;
myTrackOffset = GetTrackOffset(myTrack);
myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack);
// allocate some storage to hold the sample description for the text track
myTextDesc = (TextDescriptionHandle)NewHandle(4);
if (myTextDesc == NULL)
goto bail;
mySample = NewHandle(0);
if (mySample == NULL)
goto bail;
myErr = GetMediaSample(myMedia, mySample, 0, NULL, myMediaTime, NULL, &mySampleDuration, (SampleDescriptionHandle)myTextDesc, NULL, 1, NULL, &mySampleFlags);
if (myErr != noErr)
goto bail;
//////////
//
// add hypertext actions to the first media sample
//
//////////
// create an action container for hypertext actions
myErr = AddHTAct_CreateHyperTextActionContainer(&myActions);
if (myErr != noErr)
goto bail;
// add hypertext actions actions to sample
myErr = AddHTAct_AddHyperActionsToSample(mySample, myActions);
if (myErr != noErr)
goto bail;
//////////
//
// replace sample in media
//
//////////
myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset);
if (GetMoviesError() != noErr)
goto bail;
GetTrackNextInterestingTime(myTrack, nextTimeMediaSample | nextTimeEdgeOK, myTrackOffset, fixed1, NULL, &mySelectionDuration);
if (GetMoviesError() != noErr)
goto bail;
myErr = DeleteTrackSegment(myTrack, myTrackOffset, mySelectionDuration);
if (myErr != noErr)
goto bail;
myErr = BeginMediaEdits(myMedia);
if (myErr != noErr)
goto bail;
myErr = AddMediaSample( myMedia,
mySample,
0,
GetHandleSize(mySample),
mySampleDuration,
(SampleDescriptionHandle)myTextDesc,
1,
mySampleFlags,
&myNewMediaTime);
if (myErr != noErr)
goto bail;
myErr = EndMediaEdits(myMedia);
if (myErr != noErr)
goto bail;
// add the media to the track
myErr = InsertMediaIntoTrack(myTrack, myTrackOffset, myNewMediaTime, mySelectionDuration, myTrackEditRate);
if (myErr != noErr)
goto bail;
//////////
//
// update the movie resource
//
//////////
myErr = UpdateMovieResource(myMovie, myResRefNum, myResID, NULL);
if (myErr != noErr)
goto bail;
// close the movie file
myErr = CloseMovieFile(myResRefNum);
bail:
if (myActions != NULL)
(void)QTDisposeAtomContainer(myActions);
if (mySample != NULL)
DisposeHandle(mySample);
if (myTextDesc != NULL)
DisposeHandle((Handle)myTextDesc);
if (myMovie != NULL)
DisposeMovie(myMovie);
return(myErr);
}
The obvious bug is that, if an error happens at any point after the movie file has been successfully opened, it will not be closed. Remember what I said above about Type A and Type B brain cells? This is a classic case: it takes Type-A brain cells to recognize that the bail label is misplaced relative to the CloseMovieFile call, whereas in a structured representation, with proper use of indentation, it becomes a Type-B pattern-recognition task to spot that the CloseMovieFile call is in the wrong place, simply because its indentation level will be wrong.
Besides fixing this bug, my rewrite will also illustrate the use of a do-once within a do-once. My excuse for doing this here is to minimize the amount of time that the sample handle stays allocated, in case it is large (note I also put it in temporary memory, which is good practice for any highly variable memory usage). Note how, even in the presence of this extra complication, it is still easy to satisfy yourself that all the necessary cleanups will still take place, no matter what errors may occur.
(If you want a more convincing excuse for putting one do-once inside another, just wait till the example after this.)
static OSErr AddHTAct_AddHyperTextToTextMovie
(
const FSSpec * theFSSpec
)
{
short myResID;
short myResRefNum;
Movie myMovie;
Track myTrack;
Media myMedia;
TimeValue myTrackOffset;
TimeValue myMediaTime;
TimeValue mySampleDuration;
TimeValue mySelectionDuration;
TimeValue myNewMediaTime;
Fixed myTrackEditRate;
OSErr myErr;
myResRefNum = 0;
myMovie = nil;
do /*once*/
{
//////////
//
// open the movie file and get the first text track from the movie
//
//////////
// open the movie file for reading and writing
myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm);
if (myErr != noErr)
break;
myErr = NewMovieFromFile
(
/*theMovie =*/ &myMovie,
/*resRefNum =*/ myResRefNum,
/*resId =*/ &myResID,
/*resName =*/ nil,
/*newMovieFlags =*/ newMovieActive,
/*dataRefWasChanged =*/ nil
);
if (myErr != noErr)
break;
// find first text track in the movie
myTrack = GetMovieIndTrackType
(
/*theMovie =*/ myMovie,
/*index =*/ kIndexOne,
/*trackType =*/ TextMediaType,
/*flags =*/ movieTrackMediaType
);
if (myTrack == nil)
break;
//////////
//
// get first media sample in the text track
//
//////////
myMedia = GetTrackMedia(myTrack);
if (myMedia == nil)
break;
myTrackOffset = GetTrackOffset(myTrack);
myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack);
myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset);
if (GetMoviesError() != noErr)
break;
GetTrackNextInterestingTime
(
/*theTrack =*/ myTrack,
/*interestingTimeFlags =*/ nextTimeMediaSample | nextTimeEdgeOK,
/*time =*/ myTrackOffset,
/*rate =*/ fixed1,
/*interestingTime =*/ nil,
/*interestingDuration =*/ &mySelectionDuration
);
if (GetMoviesError() != noErr)
break;
myErr = DeleteTrackSegment(myTrack, myTrackOffset, mySelectionDuration);
if (myErr != noErr)
break;
{
TextDescriptionHandle myTextDesc;
Handle mySample;
short mySampleFlags;
QTAtomContainer myActions;
myTextDesc = nil;
mySample = nil;
myActions = nil;
do /*once*/
{
// allocate some storage to hold the sample description for the text track
myTextDesc = (TextDescriptionHandle)NewHandle(4);
myErr = MemError();
if (myErr != noErr)
break;
mySample = TempNewHandle(0, &myErr);
if (myErr != noErr)
break;
myErr = GetMediaSample
(
/*theMedia =*/ myMedia,
/*dataOut =*/ mySample,
/*maxSizeToGrow =*/ 0,
/*size =*/ nil,
/*time =*/ myMediaTime,
/*sampleTime =*/ nil,
/*durationPerSample =*/ &mySampleDuration,
/*sampleDescriptionH =*/ (SampleDescriptionHandle)myTextDesc,
/*sampleDescriptionIndex =*/ nil,
/*maxNumberOfSamples =*/ 1,
/*numberOfSamples =*/ nil,
/*sampleFlags =*/ &mySampleFlags
);
if (myErr != noErr)
break;
//////////
//
// add hypertext actions to the first media sample
//
//////////
// create an action container for hypertext actions
myErr = AddHTAct_CreateHyperTextActionContainer(&myActions);
if (myErr != noErr)
break;
// add hypertext actions actions to sample
myErr = AddHTAct_AddHyperActionsToSample(mySample, myActions);
if (myErr != noErr)
break;
myErr = BeginMediaEdits(myMedia);
if (myErr != noErr)
break;
myErr = AddMediaSample
(
/*theMedia =*/ myMedia,
/*dataIn =*/ mySample,
/*inOffset =*/ 0,
/*size =*/ GetHandleSize(mySample),
/*durationPerSample =*/ mySampleDuration,
/*sampleDescriptionH =*/ (SampleDescriptionHandle)myTextDesc,
/*numberOfSamples =*/ 1,
/*sampleFlags =*/ mySampleFlags,
/*sampleTime =*/ &myNewMediaTime
);
if (myErr != noErr)
break;
myErr = EndMediaEdits(myMedia);
if (myErr != noErr)
break;
}
while (false);
DisposeHandleVar(&mySample);
DisposeHandleVar((Handle *)&myTextDesc);
DisposeQTAtomContainerVar(&myActions);
}
if (myErr != noErr)
break;
//////////
//
// replace sample in media
//
//////////
// add the media to the track
myErr = InsertMediaIntoTrack
(
/*theTrack =*/ myTrack,
/*trackStart =*/ myTrackOffset,
/*mediaTime =*/ myNewMediaTime,
/*mediaDuration =*/ mySelectionDuration,
/*mediaRate =*/ myTrackEditRate
);
if (myErr != noErr)
break;
//////////
//
// update the movie resource
//
//////////
myErr = UpdateMovieResource
(
/*theMovie =*/ myMovie,
/*resRefNum =*/ myResRefNum,
/*resId =*/ myResID,
/*resName =*/ nil
);
if (myErr != noErr)
break;
// close the movie file
myErr = CloseMovieFile(myResRefNum); /* get back any error */
myResRefNum = nil; /* so I don't close it again */
}
while (false);
DisposeMovieVar(&myMovie);
CloseMovieFileVar(&myResRefNum);
return
myErr;
} /*AddHTAct_AddHyperTextToTextMovie*/
You should be able to guess how the new disposal routines are defined. If not, you can find them in my LDOLib libraries.
Note that I have been a little bit finicky about making CloseMovieFile calls. If everything has succeeded up to that point, then I save the error code from the CloseMovieFile call (which might represent an error saving the updated movie file contents to disk). Otherwise, if there has been an error elsewhere, then I rely on CloseMovieFileVar to close the file, which ignores any error it might encounter (I’ve already got a problem creating the movie, so I don’t care if it’s being successfully saved to disk or not). Personally, I would prefer it if the API provided a separate FlushMovieFileChangesToDisk call, which would return the appropriate error. Then I could always safely ignore any error from CloseMovieFile. (And if I didn’t make the FlushMovieFileChangesToDisk call first, then that would mean that I didn’t care if the changes were saved to disk or not.)
Yes, that is certainly not a trivial example. Remember, this isn’t about classroom exercises for kiddies, it’s about real-world programming!
Here’s another, even more complex example, taken from the file FSpCompat.c in <ftp://ftp.apple.com/developer/Sample_Code/Files/MoreFiles.sit>. Here, the cleanup doesn’t involve disposing of memory, but restoring files to their previous state. But the same principles apply:
pascal OSErr FSpExchangeFilesCompat(const FSSpec *source,
const FSSpec *dest)
{
#if !__MACOSSEVENFIVEORLATER
if (
#if !__MACOSSEVENORLATER
!FSHasFSSpecCalls() ||
#endif /* !__MACOSSEVENORLATER */
!HasFSpExchangeFilesCompatibilityFix() )
{
HParamBlockRec pb;
CInfoPBRec catInfoSource, catInfoDest;
OSErr result, result2;
Str31 unique1, unique2;
StringPtr unique1Ptr, unique2Ptr, swapola;
GetVolParmsInfoBuffer volInfo;
long theSeed, temp;
/* Make sure the source and destination are on the same volume */
if ( source->vRefNum != dest->vRefNum )
{
result = diffVolErr;
goto errorExit3;
}
/* Try PBExchangeFiles first since it preserves the file ID reference */
pb.fidParam.ioNamePtr = (StringPtr) &(source->name);
pb.fidParam.ioVRefNum = source->vRefNum;
pb.fidParam.ioDestNamePtr = (StringPtr) &(dest->name);
pb.fidParam.ioDestDirID = dest->parID;
pb.fidParam.ioSrcDirID = source->parID;
result = PBExchangeFilesSync(&pb);
/* Note: The compatibility case won't work for files with *Btree control blocks. */
/* Right now the only *Btree files are created by the system. */
if ( result != noErr )
{
pb.ioParam.ioNamePtr = NULL;
pb.ioParam.ioBuffer = (Ptr) &volInfo;
pb.ioParam.ioReqCount = sizeof(volInfo);
result2 = PBHGetVolParmsSync(&pb);
/* continue if volume has no fileID support (or no GetVolParms support) */
if ( (result2 == noErr) && hasFileIDs(&volInfo) )
{
goto errorExit3;
}
/* Get the catalog information for each file */
/* and make sure both files are *really* files */
catInfoSource.hFileInfo.ioVRefNum = source->vRefNum;
catInfoSource.hFileInfo.ioFDirIndex = 0;
catInfoSource.hFileInfo.ioNamePtr = (StringPtr) &(source->name);
catInfoSource.hFileInfo.ioDirID = source->parID;
catInfoSource.hFileInfo.ioACUser = 0; /* ioACUser used to be filler2 */
result = PBGetCatInfoSync(&catInfoSource);
if ( result != noErr )
{
goto errorExit3;
}
if ( (catInfoSource.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0 )
{
result = notAFileErr;
goto errorExit3;
}
catInfoDest.hFileInfo.ioVRefNum = dest->vRefNum;
catInfoDest.hFileInfo.ioFDirIndex = 0;
catInfoDest.hFileInfo.ioNamePtr = (StringPtr) &(dest->name);
catInfoDest.hFileInfo.ioDirID = dest->parID;
catInfoDest.hFileInfo.ioACUser = 0; /* ioACUser used to be filler2 */
result = PBGetCatInfoSync(&catInfoDest);
if ( result != noErr )
{
goto errorExit3;
}
if ( (catInfoDest.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0 )
{
result = notAFileErr;
goto errorExit3;
}
/* generate 2 filenames that are unique in both directories */
theSeed = 0x64666A6C; /* a fine unlikely filename */
unique1Ptr = (StringPtr)&unique1;
unique2Ptr = (StringPtr)&unique2;
result = GenerateUniqueName(source->vRefNum, &theSeed, source->parID, dest->parID, unique1Ptr);
if ( result != noErr )
{
goto errorExit3;
}
GenerateUniqueName(source->vRefNum, &theSeed, source->parID, dest->parID, unique2Ptr);
if ( result != noErr )
{
goto errorExit3;
}
/* rename source to unique1 */
pb.fileParam.ioNamePtr = (StringPtr) &(source->name);
pb.ioParam.ioMisc = (Ptr) unique1Ptr;
pb.ioParam.ioVersNum = 0;
result = PBHRenameSync(&pb);
if ( result != noErr )
{
goto errorExit3;
}
/* rename dest to unique2 */
pb.ioParam.ioMisc = (Ptr) unique2Ptr;
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = (StringPtr) &(dest->name);
pb.fileParam.ioDirID = dest->parID;
result = PBHRenameSync(&pb);
if ( result != noErr )
{
goto errorExit2; /* back out gracefully by renaming unique1 back to source */
}
/* If files are not in same directory, swap their locations */
if ( source->parID != dest->parID )
{
/* move source file to dest directory */
pb.copyParam.ioNamePtr = unique1Ptr;
pb.copyParam.ioNewName = NULL;
pb.copyParam.ioNewDirID = dest->parID;
pb.copyParam.ioDirID = source->parID;
result = PBCatMoveSync((CMovePBPtr) &pb);
if ( result != noErr )
{
goto errorExit1; /* back out gracefully by renaming both files to original names */
}
/* move dest file to source directory */
pb.copyParam.ioNamePtr = unique2Ptr;
pb.copyParam.ioNewDirID = source->parID;
pb.copyParam.ioDirID = dest->parID;
result = PBCatMoveSync((CMovePBPtr) &pb);
if ( result != noErr)
{
/* life is very bad. We'll at least try to move source back */
pb.copyParam.ioNamePtr = unique1Ptr;
pb.copyParam.ioNewName = NULL;
pb.copyParam.ioNewDirID = source->parID;
pb.copyParam.ioDirID = dest->parID;
(void) PBCatMoveSync((CMovePBPtr) &pb); /* ignore errors */
goto errorExit1; /* back out gracefully by renaming both files to original names */
}
}
/* Make unique1Ptr point to file in source->parID */
/* and unique2Ptr point to file in dest->parID */
/* This lets us fall through to the rename code below */
swapola = unique1Ptr;
unique1Ptr = unique2Ptr;
unique2Ptr = swapola;
/* At this point, the files are in their new locations (if they were moved) */
/* Source is named Unique1 (name pointed to by unique2Ptr) and is in dest->parID */
/* Dest is named Unique2 (name pointed to by unique1Ptr) and is in source->parID */
/* Need to swap attributes except mod date and swap names */
/* swap the catalog info by re-aiming the CInfoPB's */
catInfoSource.hFileInfo.ioNamePtr = unique1Ptr;
catInfoDest.hFileInfo.ioNamePtr = unique2Ptr;
catInfoSource.hFileInfo.ioDirID = source->parID;
catInfoDest.hFileInfo.ioDirID = dest->parID;
/* Swap the original mod dates with each file */
temp = catInfoSource.hFileInfo.ioFlMdDat;
catInfoSource.hFileInfo.ioFlMdDat = catInfoDest.hFileInfo.ioFlMdDat;
catInfoDest.hFileInfo.ioFlMdDat = temp;
/* Here's the swap (ignore errors) */
(void) PBSetCatInfoSync(&catInfoSource);
(void) PBSetCatInfoSync(&catInfoDest);
/* rename unique2 back to dest */
errorExit1:
pb.ioParam.ioMisc = (Ptr) &(dest->name);
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = unique2Ptr;
pb.fileParam.ioDirID = dest->parID;
(void) PBHRenameSync(&pb); /* ignore errors */
&nsbp;
/* rename unique1 back to source */
errorExit2:
pb.ioParam.ioMisc = (Ptr) &(source->name);
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = unique1Ptr;
pb.fileParam.ioDirID = source->parID;
(void) PBHRenameSync(&pb); /* ignore errors */
}
errorExit3: { /* null statement */ }
return ( result );
}
else
#endif /* !__MACOSSEVENFIVEORLATER */
{
return ( FSpExchangeFiles(source, dest) );
}
}
This routine has three separate levels of cleanup, identified by the labels errorExit1, errorExit2 and errorExit3: it is not clear at all from the topology of the gotos referencing these labels that in fact the cleanup levels nest within one another. This is what structured programming was designed to make clear.
Here’s how I would rewrite the above code, using my do-once constructs (and also fixing the bug with not checking the error status from the second GenerateUniqueName call):
pascal OSErr FSpExchangeFilesCompat
(
const FSSpec * source,
const FSSpec * dest
)
{
OSErr result;
#if !__MACOSSEVENFIVEORLATER
if
(
#if !__MACOSSEVENORLATER
!FSHasFSSpecCalls()
||
#endif /* !__MACOSSEVENORLATER */
!HasFSpExchangeFilesCompatibilityFix()
)
{
HParamBlockRec pb;
CInfoPBRec catInfoSource, catInfoDest;
OSErr result2;
Str31 unique1, unique2;
StringPtr unique1Ptr, unique2Ptr, swapola;
GetVolParmsInfoBuffer volInfo;
long theSeed, temp;
do /*once*/
{
/* Make sure the source and destination are on the same volume */
if (source->vRefNum != dest->vRefNum)
{
result = diffVolErr;
break;
} /*if*/
/* Try PBExchangeFiles first since it preserves the file ID reference */
pb.fidParam.ioNamePtr = (StringPtr)source->name;
pb.fidParam.ioVRefNum = source->vRefNum;
pb.fidParam.ioDestNamePtr = (StringPtr)dest->name;
pb.fidParam.ioDestDirID = dest->parID;
pb.fidParam.ioSrcDirID = source->parID;
result = PBExchangeFilesSync(&pb);
/* Note: The compatibility case won't work for files with *Btree control blocks. */
/* Right now the only *Btree files are created by the system. */
if (result == noErr)
break;
pb.ioParam.ioNamePtr = NULL;
pb.ioParam.ioBuffer = (Ptr)&volInfo;
pb.ioParam.ioReqCount = sizeof(volInfo);
result2 = PBHGetVolParmsSync(&pb);
/* continue if volume has no fileID support (or no GetVolParms support) */
if (result2 == noErr && hasFileIDs(&volInfo))
break;
/* Get the catalog information for each file */
/* and make sure both files are *really* files */
catInfoSource.hFileInfo.ioVRefNum = source->vRefNum;
catInfoSource.hFileInfo.ioFDirIndex = 0;
catInfoSource.hFileInfo.ioNamePtr = (StringPtr)source->name;
catInfoSource.hFileInfo.ioDirID = source->parID;
catInfoSource.hFileInfo.ioACUser = 0; /* ioACUser used to be filler2 */
result = PBGetCatInfoSync(&catInfoSource);
if (result != noErr)
break;
if ((catInfoSource.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0)
{
result = notAFileErr;
break;
} /*if*/
catInfoDest.hFileInfo.ioVRefNum = dest->vRefNum;
catInfoDest.hFileInfo.ioFDirIndex = 0;
catInfoDest.hFileInfo.ioNamePtr = (StringPtr)dest->name;
catInfoDest.hFileInfo.ioDirID = dest->parID;
catInfoDest.hFileInfo.ioACUser = 0; /* ioACUser used to be filler2 */
result = PBGetCatInfoSync(&catInfoDest);
if (result != noErr)
break;
if ((catInfoDest.hFileInfo.ioFlAttrib & kioFlAttribDirMask) != 0)
{
result = notAFileErr;
break;
} /*if*/
/* generate 2 filenames that are unique in both directories */
theSeed = 0x64666A6C; /* a fine unlikely filename */
unique1Ptr = (StringPtr)&unique1;
unique2Ptr = (StringPtr)&unique2;
result = GenerateUniqueName
(
/*volume =*/ source->vRefNum,
/*startSeed =*/ &theSeed,
/*dir1 =*/ source->parID,
/*dir2 =*/ dest->parID,
/*uniqueName =*/ unique1Ptr
);
if (result != noErr)
break;
result = GenerateUniqueName
(
/*volume =*/ source->vRefNum,
/*startSeed =*/ &theSeed,
/*dir1 =*/ source->parID,
/*dir2 =*/ dest->parID,
/*uniqueName =*/ unique2Ptr
);
if (result != noErr)
break;
/* rename source to unique1 */
pb.fileParam.ioNamePtr = (StringPtr)source->name;
pb.ioParam.ioMisc = (Ptr)unique1Ptr;
pb.ioParam.ioVersNum = 0;
result = PBHRenameSync(&pb);
if (result != noErr)
break;
do /*once*/
{ /* back out gracefully by renaming unique1 back to source */
/* rename dest to unique2 */
pb.ioParam.ioMisc = (Ptr)unique2Ptr;
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = (StringPtr)dest->name;
pb.fileParam.ioDirID = dest->parID;
result = PBHRenameSync(&pb);
if (result != noErr)
break;
do /*once*/
{ /* back out gracefully by renaming both files to original names */
/* If files are not in same directory, swap their locations */
if (source->parID != dest->parID)
{
/* move source file to dest directory */
pb.copyParam.ioNamePtr = unique1Ptr;
pb.copyParam.ioNewName = NULL;
pb.copyParam.ioNewDirID = dest->parID;
pb.copyParam.ioDirID = source->parID;
result = PBCatMoveSync((CMovePBPtr)&pb);
if (result != noErr)
break;
/* move dest file to source directory */
pb.copyParam.ioNamePtr = unique2Ptr;
pb.copyParam.ioNewDirID = source->parID;
pb.copyParam.ioDirID = dest->parID;
result = PBCatMoveSync((CMovePBPtr) &pb);
if (result != noErr)
{
/* life is very bad. We'll at least try to move source back */
pb.copyParam.ioNamePtr = unique1Ptr;
pb.copyParam.ioNewName = NULL;
pb.copyParam.ioNewDirID = source->parID;
pb.copyParam.ioDirID = dest->parID;
(void)PBCatMoveSync((CMovePBPtr)&pb); /* ignore errors */
break;
} /*if*/
} /*if*/
/* Make unique1Ptr point to file in source->parID */
/* and unique2Ptr point to file in dest->parID */
/* This lets us fall through to the rename code below */
swapola = unique1Ptr;
unique1Ptr = unique2Ptr;
unique2Ptr = swapola;
/* At this point, the files are in their new locations (if they were moved) */
/* Source is named Unique1 (name pointed to by unique2Ptr) and is in dest->parID */
/* Dest is named Unique2 (name pointed to by unique1Ptr) and is in source->parID */
/* Need to swap attributes except mod date and swap names */
/* swap the catalog info by re-aiming the CInfoPB's */
catInfoSource.hFileInfo.ioNamePtr = unique1Ptr;
catInfoDest.hFileInfo.ioNamePtr = unique2Ptr;
catInfoSource.hFileInfo.ioDirID = source->parID;
catInfoDest.hFileInfo.ioDirID = dest->parID;
/* Swap the original mod dates with each file */
temp = catInfoSource.hFileInfo.ioFlMdDat;
catInfoSource.hFileInfo.ioFlMdDat = catInfoDest.hFileInfo.ioFlMdDat;
catInfoDest.hFileInfo.ioFlMdDat = temp;
/* Here's the swap (ignore errors) */
(void)PBSetCatInfoSync(&catInfoSource);
(void)PBSetCatInfoSync(&catInfoDest);
}
while (false);
/* rename unique2 back to dest */
pb.ioParam.ioMisc = (Ptr)dest->name;
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = unique2Ptr;
pb.fileParam.ioDirID = dest->parID;
(void)PBHRenameSync(&pb); /* ignore errors */
}
while (false);
/* rename unique1 back to source */
pb.ioParam.ioMisc = (Ptr)source->name;
pb.ioParam.ioVersNum = 0;
pb.fileParam.ioNamePtr = unique1Ptr;
pb.fileParam.ioDirID = source->parID;
(void)PBHRenameSync(&pb); /* ignore errors */
}
while (false);
}
else
#endif /* !__MACOSSEVENFIVEORLATER */
{
result = FSpExchangeFiles(source, dest);
} /*if*/
return
result;
} /*FSpExchangeFilesCompat*/
A little bit clearer, don’t you think?
Now, a couple of examples, from my aforementioned LDOLib libraries, illustrating some slightly more advanced points.
void SendMyselfOpenEvent
(
uint NrFiles,
const FSSpec * FilesToOpen, /* array */
Boolean CanInteract,
StringID LocationID
)
/* sends myself an open-document AppleEvent to open the specified file(s).
Global strings required: ErrorEncounteredStringKey, StdOKStringKey. */
{
AppleEvent TheEvent;
AEDesc ToOpenDesc, ToOpenList;
uint FileIndex;
InitAEDesc(&TheEvent);
InitAEDesc(&ToOpenDesc);
InitAEDesc(&ToOpenList);
do /*once*/
{
CreateSelfEvent
(
/*EventClass =*/ kCoreEventClass,
/*EventID =*/ kAEOpenDocuments,
/*TransactionID =*/ kAnyTransactionID,
/*Result =*/ &TheEvent,
/*Err =*/ &Err
);
if (Err != noErr)
break;
if (NrFiles == 1)
{
Err = NewAlias
(
/*fromFile =*/ nil,
/*target =*/ FilesToOpen,
/*alias =*/ (AliasHandle *)&ToOpenDesc.dataHandle
);
if (Err != noErr)
break;
ToOpenDesc.descriptorType = typeAlias;
Err = AEPutParamDesc
(
/*theAppleEvent =*/ &TheEvent,
/*theAEKeyword =*/ keyDirectObject,
/*theAEDesc =*/ &ToOpenDesc
);
}
else
{
NewAEList(&ToOpenList, &Err);
if (Err != noErr)
break;
FileIndex = 0;
for (;;)
{
if (FileIndex == NrFiles)
break;
DisposeAEDesc(&ToOpenDesc);
Err = NewAlias
(
/*fromFile =*/ nil,
/*target =*/ FilesToOpen + FileIndex,
/*alias =*/ (AliasHandle *)&ToOpenDesc.dataHandle
);
if (Err != noErr)
break;
ToOpenDesc.descriptorType = typeAlias;
Err = AEPutDesc
(
/*theAEDescList =*/ &ToOpenList,
/*index =*/ 0,
/*theAEDesc =*/ &ToOpenDesc
);
if (Err != noErr)
break;
++FileIndex;
} /*for*/
if (Err != noErr)
break;
Err = AEPutParamDesc
(
/*theAppleEvent =*/ &TheEvent,
/*theAEKeyword =*/ keyDirectObject,
/*theAEDesc =*/ &ToOpenList
);
} /*if*/
if (Err != noErr)
break;
SendEventWithStatus(&TheEvent, CanInteract, LocationID);
}
while (false);
DisposeAEDesc(&ToOpenList);
DisposeAEDesc(&ToOpenDesc);
DisposeAEDesc(&TheEvent);
} /*SendMyselfOpenEvent*/
This routine constructs an “open documents” AppleEvent, requesting the open of the specified list of files, which the application sends to itself for script-recording purposes.
InitAEDesc is a simple routine which fills the fields of an AppleEvent descriptor with null values:
void InitAEDesc
(
AEDesc * TheDesc
)
/* initializes an AEDesc to be the null AppleEvent descriptor. */
{
TheDesc->descriptorType = typeNull;
TheDesc->dataHandle = nil;
} /*InitAEDesc*/
Note how the idempotency of disposal operations is exploited in the for-loop which builds the list of aliases of the files to be opened, in the case where there is more than one. There will typically be one redundant disposal of ToOpenDesc, but this is harmless, and the code is easier to write this way.
static pascal OSErr AccessContentsProperty
(
DescType DesiredClass,
AEDesc * Container,
DescType ContainerClass,
DescType KeyForm,
const AEDesc * KeyData,
AEToken * TheToken,
RefConType TheRefCon
)
/* accessor routine to implement the "contents" property of
an object as a record with fields corresponding to the actual
individual properties. */
{
OSErr Err;
const THz PreviousZone = GetZone();
CleanupList PropertyTokenCleanup;
AERecord PropertyAccessors, PropertyValue;
ContentsPropertyEntryHandle ContentsTokens;
SInt32 NrProperties, PropertyIndex;
AEKeyword PropertyID;
AccessorProcDesc ThisPropertyAccessor;
AEDesc PropertyIDDesc, ThisPropertyValue;
AEToken ThisPropertyToken;
OSType ActualType;
Size ActualSize;
PropertyTokenCleanup = nil;
InitAEDesc(&PropertyAccessors);
InitAEDesc(&PropertyValue);
ContentsTokens = nil;
InitAEDesc(&PropertyIDDesc);
InitAEDesc(&ThisPropertyToken);
InitAEDesc(&ThisPropertyValue);
do /*once*/
{
if (Container->descriptorType == TypeToken)
{
Container->descriptorType = typeAERecord;
Err = AEGetKeyDesc
(
/*theAERecord =*/ Container,
/*theAEKeyword =*/ PropertyAccessorProcID,
/*desiredType =*/ typeAERecord,
/*result =*/ &PropertyAccessors
);
Container->descriptorType = TypeToken;
}
else
{
ShouldntOccur();
Err = errAEEventNotHandled;
} /*if*/
if (Err != noErr)
break;
CreateAEToken(TheToken, &Err);
if (Err != noErr)
break;
NewCleanupList(&PropertyTokenCleanup, &Err);
if (Err != noErr)
break;
SetCleanupList(TheToken, PropertyTokenCleanup, &Err);
if (Err != noErr)
break;
ContentsTokens = (ContentsPropertyEntryHandle)NewHandle(0);
Err = MemError();
if (Err != noErr)
break;
SetZone(SystemZone());
NewAERecord(&PropertyValue, &Err);
SetZone(PreviousZone);
if (Err != noErr)
break;
Err = AECountItems(&PropertyAccessors, &NrProperties);
if (Err != noErr)
break;
PropertyIndex = 0;
for (;;)
{
if (PropertyIndex == NrProperties)
break;
++PropertyIndex;
Err = AEGetNthPtr
(
/*theAEDescList =*/ &PropertyAccessors,
/*index =*/ PropertyIndex,
/*desiredType =*/ TypeAccessorProcDesc,
/*theAEKeyword =*/ &PropertyID,
/*typeCode =*/ &ActualType,
/*data =*/ Descr(ThisPropertyAccessor),
/*actualSize =*/ &ActualSize
);
if (Err != noErr)
break;
DisposeAEDesc(&PropertyIDDesc);
Err = AECreateDesc
(
/*typeCode =*/ typeType,
/*data =*/ Descr(PropertyID),
/*result =*/ &PropertyIDDesc
);
if (Err != noErr)
break;
(void)DisposeAEToken(&ThisPropertyToken);
Err = (*ThisPropertyAccessor.Access)
(
/*desiredClass =*/ typeWildCard,
/*container =*/ Container,
/*containerClass =*/ ContainerClass,
/*form =*/ formPropertyID,
/*selectionData =*/ &PropertyIDDesc,
/*value =*/ &ThisPropertyToken,
/*accessorRefcon =*/ ThisPropertyAccessor.RefCon
);
if (Err != noErr)
break;
DisposeAEDesc(&ThisPropertyValue);
GetData
(
/*TheToken =*/ &ThisPropertyToken,
/*DesiredType =*/ typeWildCard,
/*UseSysHeap =*/ true,
/*Dest =*/ &ThisPropertyValue,
/*Err =*/ &Err
);
if (Err != noErr)
break;
SetHandleSize
(
(Handle)ContentsTokens,
PropertyIndex * sizeof(ContentsPropertyEntry)
);
Err = MemError();
if (Err != noErr)
break;
(*ContentsTokens)[PropertyIndex - 1].PropertyID = PropertyID;
(*ContentsTokens)[PropertyIndex - 1].PropertyToken = ThisPropertyToken;
/* save token for setter and cleanup routines */
InitAEDesc(&ThisPropertyToken);
/* so I don't dispose of token yet */
Err = AEPutKeyDesc
(
/*theAERecord =*/ &PropertyValue,
/*theAEKeyword =*/ PropertyID,
/*theAEDesc =*/ &ThisPropertyValue
);
if (Err != noErr)
break;
} /*for*/
if (Err != noErr)
break;
SetDataDescriptorField(TheToken, &PropertyValue, &Err);
SetSetDataRoutineField
(
/*TheToken =*/ TheToken,
/*SetData =*/ (SetDataRoutine)SetContentsProperty,
/*RefCon =*/ (RefConType)ContentsTokens,
/*Err =*/ &Err
);
AddCleanupAction
(
/*ToList =*/ PropertyTokenCleanup,
/*Action =*/ (CleanupAction)CleanupContentsTokens,
/*ActionArg =*/ (RefConType)ContentsTokens,
/*Err =*/ &Err
);
if (Err != noErr)
break;
ContentsTokens = nil; /* so I don't dispose of it yet */
}
while (false);
DisposeAEDesc(&ThisPropertyValue);
(void)DisposeAEToken(&ThisPropertyToken);
DisposeHandleVar((Handle *)&ContentsTokens);
DisposeAEDesc(&PropertyIDDesc);
DisposeAEDesc(&PropertyValue);
DisposeAEDesc(&PropertyAccessors);
DisposeCleanupList(PropertyTokenCleanup);
return
Err;
} /*AccessContentsProperty*/
This is an “object accessor” routine, used with the AppleEvent Object Support Library to implement a “contents” property for objects in an AppleScriptable application. Whatever other properties the client application may define for an AppleScript object, the above routine, as part of my scriptability framework, automatically adds a contents property which gives access to all the values of those other properties as a record.
The way this routine works is that it gets the list of property accessors, which are defined by the client application and attached to the token for the object, and uses them to build a list of corresponding tokens for the individual properties. The “CleanupList” object is just a general way of managing a reference count (defined in my libraries), with automatic invocation of any desired disposal actions when the reference count goes to zero. Here it is used to dispose of the property tokens when the object token itself is disposed.
Note again the reliance on idempotency of disposal operations in the for-loop which retrieves the individual property tokens. This loop contains four calls to routines which explicitly allocate memory, along with two other calls which can also return errors. The variables which are used to hold new memory which is allocated in the loop (PropertyIDDesc, ThisPropertyToken and ThisPropertyValue) are disposed before each allocation within the loop, and again in the final disposal section of the routine. The extra disposal calls do no harm, and putting them all in adds fewer complications to the code than trying to figure out which ones are really necessary under all conditions!
Note also how responsibility for allocated memory is systematically passed from one object to another, and never left in limbo: each property token is initially created in ThisPropertyToken, then after it is stored in the ContentsTokens array, ThisPropertyToken is set to the null token, so the token will not be disposed until the ContentsTokens array is later disposed. And similarly, once the ContentsTokens array has been fully constructed and successfully attached to the cleanup list, ContentsTokens is set to nil to mark that this routine is no longer responsible for that structure.
There is one subtle point to note about the final disposal of PropertyTokenCleanup: when the cleanup object is initially created, it has a reference count of 1. Then, when it is attached to TheToken by the SetCleanupList call, its reference count is incremented to 2. Thus, the DisposeCleanupList call at the end of the above routine indicates that this routine has relinquished its responsibility for the cleanup object (and all that is attached to it), but responsibility for one remaining reference count to it still resides with whoever retains access to TheToken.
I’ll admit, there are a couple of errors that are not checked, namely those from the SetDataDescriptorField and SetSetDataRoutineField calls. I didn’t bother, because errors from these calls will indicate an incompletely-constructed token, which is not a dangerous or fatal situation. Besides, if they can’t get enough memory for their modest needs, then the system is in such dire straits that a truly fatal error will occur soon enough!
Yes, there is some intricate choreography going on, to ensure that errors are always handled gracefully, without ever losing track of memory (or corrupting it!). And yet, in spite of all that, the code still retains a satisfying degree of simplicity and readability, which gives you confidence that it will really work as intended.
Actually, Brooks did mention, in The Mythical Man-Month, a way in which programmers could achieve another order-of-magnitude improvement in their productivity. He called it metaprogramming, but the more familiar term to most of us is scripting. That is, application programs make their functionality available in the form of primitives that can be used in a language that operates at a higher level than the one the programs themselves are written in. Brooks specifically mentioned AppleScript as a good example of this. (Unix folks may think of Perl, but I think that the typical level of communication between Perl and other programs—via pipes passing unstructured ASCII bytestreams—is at too low a level for it to be accepted as comparable to AppleScript.) The idea was in its relative infancy in 1995, but it has grown somewhat since then, though it is still not quite as widely embraced as it should be.
Certainly, scripting is no excuse not to get those lower-level program functions working right in the first place—after all, if they don’t work right, how can you script them?
Created 2002 July 7 by Lawrence D’Oliveiro, last modified 2002 July 12.