April 90 - USING OBJECTS SAFELY IN OBJECT PASCAL
USING OBJECTS SAFELY IN OBJECT PASCAL
CURT BIANCHI
In Object Pascal, objects are just like handles in that they refer to relocatable blocks
of memory. To use objects safely, the programmer needs to recognize that the
Macintosh Memory Manager can move the block of memory referred to by an object or
handle, although only at well-defined times. This article gives guidelines for the safe
use of objects in ObjectPascal.
The simplicity and elegance of Object Pascal's syntax is a two-edged sword. On the one
hand, it makes Object Pascal feel like a natural extension to Pascal; on the other, it can
lull a programmer into a false sense of security. For although the syntax of Object
Pascal treats objects as though they were statically allocated, the fact is that in Object
Pascal, objects arealways allocated as relocatable blocks (handles, in the vernacular)
in the application heap. Thus, when you write Object Pascal programs for the
Macintosh, you must be eternally aware that objects are handles, and program
accordingly. This article tells you how to do that with MPW Pascal and TML Pascal, two
compilers that can be used with MacApp in the MPW environment. In addition, it gives
some tips for using handles outside the context of objects.
HOW OBJECT PASCAL IMPLEMENTS OBJECTS: A CAUTIONARY
TALE
To get an idea of how Object Pascal implements objects, let's compare the code
fragments in Figure 1. Each column of code accomplishes the same thing: the definition
and use of a data structure representing a graphical shape. The only difference is that
the left column is implemented with objects, while the right column is implemented
with handles. The code in these two columns is very similar, and a comparison of the
two reveals what goes on behind the scenes.
1 TYPE
2 TShape = OBJECT (TObject)
3 fBounds: Rect;
4 fColor: RGBColor;
5 END;
6
7
8
9 VAR
10 aShape: TShape;
11 sameShape, copiedShape: TShape;
12
13 BEGIN
14 NEW(aShape);
15 FailNIL(aShape);
16
17 aShape.fBounds := gZeroRect;
18 aShape.fColor := gRGBBlack;
19
20 sameShape := aShape;
21
22 copiedShape := TShape(aShape.Clone);
23
24 FailNIL(copiedShape);
25
26 END;
1 TYPE
2 TShapeHdl = ^TShapePtr;
3 TShapePtr = ^TShape;
4 TShape = RECORD
5 fBounds: Rect;
6 fColor: RGBColor;
7 END;
8
9 VAR
10 aShape: TShapeHdl;
11 sameShape, copiedShape: TShapeHdl;
12
13 BEGIN
14 aShape := TShapeHdl(NewHandle(SIZEOF(TShape)));
15 FailNIL(aShape);
16
17 aShape^^.fBounds := gZeroRect;
18 aShape^^.fColor := gRGBBlack;
19
20 sameShape := aShape;
21
22 copiedShape := aShape;
23 FailOSErr(HandToHand(Handle(copiedShape)));
24 FailNIL(copiedShape);
25
26 END;
Figure 1.
A Comparison of Code Implemented with Objects vs. Handles
The first thing to observe is that any variable of an object type is actually areference
to an object. That is, the variable is a handle that refers to a block of memory
containing the object's data. Thus, in the left column the value of the variableaShape is
a handle. It contains the address of a master pointer that in turn points to the object's
data. The size of the variableaShape is four bytes--the size of an address and not the
size of the object itself. This is very much the same as the right column, in which the
variableaShape is explicitly declared to be a handle. In fact, the only difference
between the two is that the object version ofTShape has an implicit field containing the
object's class ID, located just before the first declared field. The class ID is an integer
value that allows the object's type to be identified at run time.
Line 14 of each column shows how a TShape data structure is created. Since handles
must be dynamically allocated in the heap, it follows that objects must be dynamically
allocated as well. This is the purpose of the call to NEW in the left column. Note that
NEW works completely differently for objects and for other kinds of memory allocation.
For objects, NEWgenerates a call to the internal library procedure %_OBNEW, which,
aside from some debugging details, simply calls NewHandle, just like the handle-based code on the right does.
The call to FailNIL in line 15 detects the case where allocation of the object or handle
fails. FailNILis part of MacApp's failure-handling library and will be discussed in
greater detail later.
Lines 17 and 18 reference fields of aShape. In the object code, the syntax leads you to
believe that no handle dereferencing takes place, but of course we know better. What
the Pascal compiler does is to implicitly dereference the handle for you. In other
words, it does the very same thing as the code in the right column does explicitly.
Line 20 assigns one object reference to another, causing both aShapeand sameShape
to refer to thesame object. Line 22 (plus 23 in the right column) produces another
shape whose contents are exactly the same as aShape. In the object case, the Clone
method is used to produce a copy of the object referenced by aShape; copiedShape is
assigned a reference to the newly created object. Clone is implemented by calling the
Toolbox routine HandToHand, as is used in the right column. (FailOSErris a MacApp routine that checks the result of HandToHand.) Since copying an object (or a handle) requires a memory allocation for the new object, FailNIL is used to ensure that the
copy succeeded. The moral of this story is that you have to be very careful about how
you use objects. For example, you must remember that every time you refer to a field
of an object, you're really dereferencing a handle. If you're not careful, you're likely
to wind up with a corrupt heap.
A PRIMER ON HANDLES AND THEIR PITFALLS
Handles have some interesting properties. If you've done any serious programming on
the Macintosh (and I don't mean HyperTalk), then you know what I mean. If not, then
(1) you've been spared the sorrows of a corrupt heap, and (2) you ought to get How to Write Macintosh Software , 2nd ed., by Scott Knaster (Hayden Books, 1988). Chapters
2 and 3 tell you all you need to know about handles. In the meantime, I'll give you a
thumbnail description. In the heap, relocatable blocks of memory are referenced by
double indirection, as shown in Figure 2. The first pointer (called the handle) points
to a nonrelocatable pointer (called the master pointer), which in turn points to a
block of memory. The Memory Manager can move the block of memory, and when this
happens the address in the master pointer is changed to the block's new address.
Figure 2. A Handle to a Relocatable Block
This doesn't create a problem as long as you access the block via the handle. However,
at times it's necessary or desirable for the sake of efficiency to dereference the
handle--that is, make a copy of the block's master pointer, and then use that pointer
to access the block by single indirection. And even this isn't a problem--as long as the
block of memory doesn't move.
Well, we have bad news: it's bound to move at some point, when the Memory Manager
needs to compact the heap. When this happens, the master pointer itself is correctly
updated, but your copy of it is left dangling. Now for the good news: relocatable blocks
of memory only move at certain well-defined times. Thus, the key to dereferencing
handles is knowing when the blocks of memory they point to may move.
Oh, and one more bit of bad news: the Memory Manager has no garbage collection. This
means you're responsible for disposing of handles when you've finished with them, and
making sure you don't leave any dangling pointers.
PRACTICING SAFE OBJECT USAGE
Because the Memory Manager moves blocks of memory only at certain well-defined
times, it's possible to come up with reliable guidelines for safe object usage. Keep
these guidelines firmly in mind anytime you program in Object Pascal:1. Don't pass
fields of objects as parameters to any routine unless you know it's safe.
In Pascal, when a routine is called, each parameter is passed by value or by address.