THE OPENDOC ROAD

Making the Most of Memory in OpenDoc

Troy Gaul and Vincent Lo

In Issue 28, we discussed how the OpenDoc Memory Manager works and how part
editors manage Toolbox memory. This time we'll examine ways to use memory more
efficiently in the OpenDoc environment.

We'll begin by talking about how to avoid memory leaks. Memory leaks, which can be a
problem when developing traditional Macintosh applications, are as much a concern in
OpenDoc. But because OpenDoc uses reference counting, there are a few extra things to
pay attention to. We'll also discuss how to handle parameters correctly to avoid
memory leaks, and we'll take a look at ways you can set up your part editor to
maximize memory usage.

AVOIDING MEMORY LEAKS

OpenDoc objects and part editors use a reference-counting scheme that enables
OpenDoc to keep track of which objects are in use. Each time a client acquires an object
(through the object's Acquire method), the object's reference count is incremented by
1. When the object is no longer being used, the client releases it (by calling the
object's Release method) and its reference count is decremented. The object's
reference count indicates how many references to the object are being held by clients.
When the reference count goes down to 0, the object can be destroyed without affecting
any other objects. For more information on how reference counting works in OpenDoc,
see the OpenDoc Road column in develop Issue 27, "Facilitating Part Editor Unloading."

If the acquired object doesn't get released when it should, the reference count doesn't go
to 0 and the object remains in memory until the session ends. As a result, a memory
leak occurs because the occupied memory can't be used during the session.

To avoid reference count errors, it helps to keep in mind which classes are
reference-counted and which methods affect an object's reference count. OpenDoc uses
reference counting on classes whose objects often have more than one client. These
classes are subclasses of ODRefCntObject, and many are classes that part editors
interact with directly.

In general, if a method name starts with "Acquire," the reference count of the object
named in the method is incremented when the method is called. When the object is no
longer needed, the caller should release it. For example, if a part editor calls
ODDraft::AcquireFrame to access a frame object, the reference count of the returned
frame object is incremented. After the editor is done using the frame reference, a call
to the object's Release method (ODFrame::Release) must be made to avoid a memory
leak.

Some methods return a reference-counted object without affecting the object's
reference count. These methods usually start with "Get." For example,
ODFacet::GetFrame returns the frame object with which the facet is associated without
incrementing the reference count of the frame object. In this case, the caller shouldn't
call ODFrame::Release. Typically, a Get method is used to return an invariant or
unchanged attribute of an object. In the case of ODFacet, the facet acquires and stores a
reference to its ODFrame object. This reference isn't released until the ODFacet object
is deleted. When ODFacet::GetFrame is called, ODFacet returns the stored reference to
the caller. Since this reference remains valid until the ODFacet is deleted, you can use
it as long as the ODFacet is a valid object. If you want to use the returned ODFrame
object beyond the ODFacet's lifetime, you should call Acquire on the ODFrame to ensure
that you have a valid reference to it.

The best way to avoid reference count errors is to familiarize yourself with the
OpenDoc API and understand how it affects an object's reference count. TheOpenDoc
Class Reference provides a detailed description of reference counting for each method.

Temporary objects to the rescue. The code for acquiring a reference-counted
object for a brief period of time and then releasing it turns out to be quite complicated.
Listing 1 shows how complicated it can be to handle a reference-counted object when
using exception-handling code.

______________________________

Listing 1. Handling reference-counted objects

ODFrame* frame = kODNULL;
ODVolatile(frame);
// Make sure that the frame can be used in
// the CATCH block.
SOM_TRY
   // Acquire the frame.
   frame = draft->AcquireFrame(ev, id);
   // Do something with the frame here.
   ...
   // Release it when done.
   frame->Release(ev);
SOM_CATCH_ALL
   if (frame)
      frame->Release(ev);
SOM_ENDTRY

______________________________

To help alleviate this problem, OpenDoc provides a utility library that uses
stack-based C++ objects to wrap references to OpenDoc reference-counted objects.
These C++ objects are called temporary objects. Whenever such a C++ object goes out
of scope, its destructor is called and releases the reference-counted object.

The code fragment shown in Listing 2 does the same thing as the example in Listing 1
but uses temporary objects instead. This code is simpler and less error-prone.

______________________________

Listing 2. Easier handling of reference-counted objects

SOM_TRY
   TempODFrame frame =
         draft->AcquireFrame(ev, id);
   // Do something with the frame here.
   ...
SOM_CATCH_ALL
SOM_ENDTRY

______________________________

The OpenDoc utility library provides temporary objects for 17 reference-counted
classes, including ODPart, ODFrame, ODExtension, and ODStorageUnit. For more
information on creating temporary objects, see the "Temporary Objects" section of
Appendix A in the OpenDoc Cookbook.

The OpenDoc utility library also provides temporary objects for objects that aren't
reference-counted, such as ODByteArray and ODIText. These OpenDoc types deserve
special attention in regard to memory usage.

The ODByteArray structure contains three fields: _buffer, _maximum,
and_length. The _buffer field points to a memory block whose size is indicated by
the _maximum field. _length is the number of bytes used; it has to be less than or
equal to the value of _maximum.

Generally, ODByteArray is used instead of a raw pointer because the size of the
memory block is included. This enables SOM and OpenDoc to pass data between
processes without relying on shared memory. But because the _buffer field is hidden
in the ODByteArray, the memory block can easily be forgotten. Failing to free this
memory block when an ODByteArray is deallocated creates a memory leak.

The ODIText structure stores a user-visible string. One of its fields contains the
string's format; the other is an ODByteArray that contains the text string. The memory
block in the ODByteArray needs to be freed when the ODIText structure is deallocated.

Handling in and out parameters. Memory leaks can also occur when parameters
aren't handled correctly. In an OpenDoc method, each parameter is designated as
in,out, or inout.

To determine a particular parameter's designation, you can check the ".idl" files, or
see the OpenDoc Class Referencefor detailed information on each parameter.

The parameter's designation defines the memory responsibility of the caller and callee.
The part editor can use memory on the stack for parameters of primitive types or
fixed-size data structures. But for strings, byte array buffers, and objects, the part
editor must use the OpenDoc Memory Manager to do the following:

If a part editor calls an OpenDoc method and doesn't deallocate theout parameter, the
memory won't be freed until the session ends, causing a memory leak.

Since it's impossible to know how a piece of memory is allocated, OpenDoc and part
editors have to use the OpenDoc Memory Manager as the common memory management
facility. This is the only way to ensure that memory allocated by OpenDoc can be freed
by the part editor and vice versa.

SETTING UP YOUR PART EDITOR

Because your part editor is used in documents with other part editors as users
construct compound documents, it's important to make the best use of memory. Let's
talk about some of the things you can do to minimize memory usage.

Keep the data section small. When creating a part (which is a shared library),
the linker will generate code and data sections. The code section contains the
instructions that make up your part editor. This section is read-only because it's
file-mapped onto read-only memory when virtual memory is in use. The data section
is stored separately because it needs to be writeable; it contains globals, static
variables, transition vectors, virtual tables, and so on.

A single in-memory or memory-mapped copy of the code section is shared by all
processes in which that code is used. The data section is handled differently: Each
process instantiates a copy of the data section, making globals per-process rather than
per-computer or per-part. Also, because there is normally one process per OpenDoc
document, a separate data section usually exists for each document that contains a part
bound to your editor. Therefore, you should control the size of your data section so that
copies of it don't take up too much memory when multiple documents are open.

Here's what you can do to keep the size of your data section down:

Reduce exports. By minimizing the number of symbols that are exported, you can
save memory. Symbol name strings are stored in the PEF (Preferred Executable
Format) container of your code fragment. If you're using a shared library that
contains a framework or set of C++ classes, you usually need to export the symbols
for each of the member functions in the shared library and import the relevant ones
into your part editor to call them or subclass them. C++ functions have long symbol
names because they include type signature information. As a result, the size of your
code fragment can increase significantly.

This is particularly noticeable if you have multiple part editors that reference the
same code, since they'll all have large tables of symbol names. You can use a tool like
DumpPEF to check what type of information your code fragment contains and how much
space it's taking up.

A workaround is to statically link classes to your part editor. This means fewer
imports and less memory used. Of course, if you do this, you lose some of the
advantages of sharing code via a shared library.

Package multiple part editors intelligently. If you're writing a suite of part
editors for end users, it's a good idea to package them as separate editor files in the
Editors folder. However, as mentioned earlier, if you want to share common code, the
amount of memory that's used by all the editors combined can be substantial.

Packaging all your part editors and the common code in a single code fragment reduces
the number of imports and exports to almost nothing. But then you can't update just
one of the editors in your suite -- you have to replace the entire shared library. It's
also detrimental if only one or two of your editors are being used, because the system
loads the entire code fragment but only a portion of it is being referenced. This isn't as
much of a problem if the user has virtual memory enabled, but without it, memory is
wasted.

To combine multiple OpenDoc part editors into one code fragment, you have to compile
the code for all of them together. You can do this either by putting them all into one
project or by having multiple projects generate static library files and a master
project that includes each of the single-part libraries. Then you need to make sure
that the ClassData symbols for all the parts are exported as separate symbols, by using
pragmas or a ".exp" file. Finally, you must include the 'nmap' resources from all the
individual editors in the combined file. Of course, the IDs of these resources can't
conflict, but since OpenDoc doesn't require any specific resource IDs for 'nmap'
resources, that shouldn't be a problem to set up.

Use SOM classes instead of C++. If you'd like to separate framework code from
part editor code (for example, to have multiple editors share the same framework or
set of classes), note that there are several advantages to using SOM classes instead of
C++ classes.

With SOM on the Mac OS, you only have to export the class's ClassData symbol. The
virtual tables are maintained by the SOM kernel; they don't exist in your data section.

Since SOM has so little overhead, you can package multiple editors as separate code
fragments (either in separate, replaceable files or in a single file). Editors that aren't
being used won't be loaded.

You can also reduce the granularity of your shared libraries, such that different
classes are in different shared libraries (again, in separate files or the same file).
This allows you to split up your framework so that only the sections that the client
needs are loaded into memory. For example, if you have one part editor that embeds
other editors and another that doesn't, but they share the same framework, the
framework's embedding code can be in a separate shared library from the code that's
needed by all part editors.

SOM supports release-to-release binary compatibility, and it deals with the fragile
base class problem of C++. It also defines a binary interface that supports languages
other than C++. Currently, emitters on the Mac OS for C and C++ are available. Other
languages can also be supported.

Don't be afraid to use SOM. Better tools for building SOM classes are being released. In
particular, Direct-to-SOM support has been added to Metrowerks CodeWarrior's C++
compiler and Apple's MrCpp, so you can build SOM classes with much less effort and
with a more familiar syntax.

Use #pragma internal. Space is also wasted when it's set aside for instructions and
never used. By default, functions on a PowerPC(TM) processor are assumed to be
external, so for the processor to jump to the routine, it's expected to go through a
TVector. To do this, the compiler leaves room in the code for the linker to add the
necessary instruction to restore the TOC (Table Of Contents) after a jump to a TVector.
If it turns out that the routine is in the same code fragment, restoring the TOC isn't
necessary, but because the space has already been inserted, the linker has little
recourse but to put a no-op instruction in that place. (The code was generated
expecting certain offsets, so the linker can't shuffle the code around easily.) Space is
then wasted for calls that are internal to the code fragment.

There are a couple of ways around this. One thing you can do is to declare a function
with the keyword static (this means that it can't be used outside the file it's defined
in) so that the compiler can tell it's an internal function. You can also use the
internal pragma in CodeWarrior. The following code marks the declaration of two
functions as internal by enclosing them in a #pragma internal block. This informs
the compiler that calls to those functions can be assumed to be internal calls, and it
won't leave space for restoring the TOC after a TVector call.

#pragma internal on
   void InternalFunction();
   ODBoolean AnotherInternalFunction(short count);
#pragma internal reset

Note that a function internal to your code can still be an export from your shared
library. In this case, your header should conditionalize its inclusion of#pragma
internal for your own use so that external clients don't mistakenly see it as internal.

This technique is not used for CFM-68K code because calls there are
assumed to be internal unless they're marked otherwise (with #pragma
import).*

The MrPlus profiling tool can also be used to get rid of unneeded no-op instructions.
You can get this tool and its documentation on the E.T.O. and MPW Pro CDs.

EVERY BYTE COUNTS

OpenDoc presents a new model for constructing software. However, many of the
techniques you've used for the traditional application model can still be applied to the
OpenDoc environment. By also incorporating some of the suggestions we've brought up
here, you'll be able to further reduce your part editor's footprint and avoid memory
leaks.

RELATED READING

This documentation is available on the OpenDoc Developer Release CD and on
the OpenDoc Web site (http://www.opendoc.apple.com).

TROY GAUL (tgaul@apple.com)  recently joined  the OpenDoc engineering team,
where he's working with JavaTM.  Having also written the sample part editor formerly
known as Cappuccino, he has a caffeine buzz that should last into the next century.*

VINCENT LO (vincent@apple.com) is Apple's technical lead for OpenDoc. Since he
recently introduced the OpenDoc team to Hong Kong cinema, it occasionally happens
that the OpenDoc engineering meeting resembles a scene from a Hong Kong action
movie.*

Thanks to Jens Alfke, David Bice, and Steve Smith for reviewing this column.*