Every account of building 68K Macintosh software repeats the same instruction: link against MacTraps, and take ANSI while you are at it. The instruction is correct but the explanation usually given for it — “MacTraps contains the Toolbox” — is wrong, and the ANSI part is frequently unnecessary. This article establishes what each library actually contributes by removing them from a working THINK C++ project and reading what the linker says.
The subject is the GfxDemo project described in the step-by-step walkthrough: a Color QuickDraw animation in C++, built with Symantec C++ 7.0 on System 7.6.1 under Basilisk II, driven over the AppleBridge link. The experiments below were performed on that live project.
1. The received wisdom, and why it cannot be right
A 68K Macintosh does not call the Toolbox through a library in the modern sense. Toolbox and Operating System entry points are A-line traps: instruction words whose high nibble is $A are unimplemented on the 68000 family and raise an exception. The Trap Dispatcher decodes the remaining bits as an index into the trap tables and vectors to the ROM (or to a patch). NewCWindow is not an address to be resolved at link time; it is opcode $A9BD.
That immediately raises a question. If Toolbox calls compile to single instructions, what is there for a library to define? The usual answer — that MacTraps is the Toolbox interface — predicts that removing it should leave every Toolbox call unresolved. That prediction is testable.
2. Experiment 1: remove MacTraps
MacTraps was removed from the project through Source ▸ Remove, leaving CPlusLib, main.cp and ANSI++. The project’s own accounting shows the loss: Segment 2 falls from 12024 to 4950 bytes, totals from 41098 to 34024.

Asking the IDE to link produces a failure — but not the failure the received wisdom predicts:

The complete set of unresolved symbols is two entries long:

undefined: screenBits
undefined: thePort
That is the entire list. The program calls InitGraf, InitFonts, InitWindows, InitMenus, TEInit, InitDialogs, InitCursor, FlushEvents, NewCWindow, SetPort, WaitNextEvent, BeginUpdate, EndUpdate, SetRect, PaintRect, PaintOval, FrameOval, PenSize, MoveTo, LineTo, RGBForeColor, TextFont, TextSize, DrawString, TickCount and DisposeWindow. Not one of them is undefined.
The reason is that THINK C emits those calls inline as the $Axxx opcodes they are. No symbol is generated, so nothing needs resolving. The compiler knows the trap words from the interface headers and plants them directly.
What screenBits and thePort have in common is that they are not calls at all. They are QuickDraw globals — data, not code.
3. What MacTraps actually supplies
QuickDraw keeps its globals in the application’s A5 world. At startup, register A5 points at the boundary between the application’s global data and its jump table; QuickDraw’s globals are placed at negative offsets from A5, growing downward, with thePort at the top of the block.
This is the origin of an idiom that puzzles newcomers to THINK C:
InitGraf(&thePort); /* THINK C — not qd.thePort */
InitGraf is passed the address of the last field so it can lay the block out beneath it. MPW-derived toolchains gather these into a QDGlobals qd structure and pass &qd.thePort; THINK C declares them as individual globals. Either way the storage must exist somewhere, and in THINK C that storage is defined by MacTraps. Hence the two undefined symbols: the demo reads thePort (via SetPort bookkeeping and InitGraf) and screenBits (to centre its window on the main screen).
So the accurate statement is:
MacTrapssupplies the A5-world data the Toolbox requires, plus glue for the traps the compiler does not inline — not the trap calls themselves.
The 7074 bytes it contributed to this project are largely that glue and the low-memory accessors, pulled in selectively by reference. A program that touched no QuickDraw globals and used only inlined traps could in principle link without it; a program that draws anything cannot, because it cannot avoid thePort.
Two corollaries follow. MacTraps2 exists for the less common trap sets, kept separate so ordinary programs do not pay for them. And a linker complaining about thePort is not telling you the Toolbox is missing — it is telling you your A5 world is unfurnished.
4. Experiment 2: remove ANSI++
MacTraps was restored and ANSI++ — 28492 bytes, the largest single item in the project — removed instead.

The rebuild succeeded. No compile errors, no link failure, no undefined symbols:

| Configuration | Total |
|---|---|
As delivered by the C++ Project model | 41098 |
Same program, ANSI++ removed | 12602 |
The program lost 69 % of its size and nothing else. ANSI++ was never required; it arrived with the project model, which is oriented toward console-style C++ programs, and it was linked because it was listed — not because anything referenced it.
(Method note: the reduction is established by a successful compile-and-link with totals recomputed. Re-running the binary in that configuration was not re-confirmed, because the Apple Event that launches it failed on a transport timeout unrelated to the link.)
5. Why the demo could shed ANSI
That result is not automatic. A typical Toolbox program does drag in the C library, and it is worth being explicit about what was done to avoid it, because each item is a decision a programmer makes.
No printf family. The frame counter needs an integer rendered as text. The obvious sprintf would pull in stdio and, transitively, buffered file I/O. NumToString is a Toolbox call but lives in a package that is not always available to a given toolchain’s link set. The demo instead converts by hand into a Pascal string:
static void NumToPStr(long v, Str255 out)
{
unsigned char tmp[12];
short n = 0, i;
if (v <= 0) { out[0] = 1; out[1] = '0'; return; }
while (v > 0 && n < 11) { tmp[n++] = (unsigned char)('0' + (v % 10)); v /= 10; }
out[0] = (unsigned char)n;
for (i = 0; i < n; i++) out[1 + i] = tmp[n - 1 - i];
}
No floating point. Circular motion wants sine and cosine. Calling them would pull in the maths library and, on 68K without an FPU, SANE — the Standard Apple Numeric Environment — which is both large and slow. The demo uses a sixteen-entry quarter-wave table with the quadrant reconstructed by reflection, all in integer arithmetic, with results scaled by 1024 and folded back with a shift:
fX = (short)(WIN_W / 2 + (((long)CosTab(a) * fRadius) >> 10));
No operator new. All shape objects have automatic storage in main and are referenced through a pointer array. This matters more than it appears: new would reference CPlusLib’s allocator, which in turn typically reaches malloc in the C library — and the ANSI dependency you were trying to avoid returns through the back door.
No global constructors. A global object with a constructor requires the static-initialisation chain to run before main, another runtime entanglement. Every object here is local.
The general rule: on this platform the C library is pulled in transitively, most often through the allocator. Avoiding it is not about avoiding #include <stdio.h>; it is about avoiding anything that allocates.
6. What CPlusLib is for
CPlusLib (1776 bytes here) remained in the project and was not tested by removal. Its role is the C++ runtime that the compiler cannot express inline: virtual-function dispatch support, the static constructor/destructor chains, operator new/delete, and the machinery around class layout. The demo exercises virtual dispatch heavily — the entire animation loop is a polymorphic call through Shape* — but avoids the allocator, which is why its contribution is measured in hundreds of bytes rather than kilobytes.
For a C++ program on this toolchain, CPlusLib is the one library you should expect to need and should not try to shed.
7. Segments, and why the project window shows two
The project window lists Segment 2 and a separate ANSI++ Segment. This is not cosmetic. 68K applications are divided into CODE resources loaded on demand by the Segment Loader, and inter-segment calls route through the jump table in the A5 world. The practical constraint is that intra-segment references are PC-relative with a 16-bit displacement, so a single segment cannot exceed 32 KB of code.
ANSI++ at 28492 bytes is close enough to that ceiling that it is given its own segment by default; putting it beside the application code would leave almost no room. The equivalent concern in MPW-based builds is met by compiling with -model far, which promotes references to 32-bit and removes the limit at a small cost in code size and speed. Either way, the underlying constraint is the same 16-bit displacement.
This also explains why the totals in the project window are the honest measure to watch: they aggregate across segments, so a change that merely moves code between them is visible as no change at all.
8. Practical guidance
For a Toolbox application in THINK C or THINK C++:
| Library | Needed? | Because |
|---|---|---|
MacTraps | Yes, for anything that draws | Defines the QuickDraw globals (thePort, screenBits) in the A5 world, plus non-inlined trap glue |
CPlusLib | Yes, for C++ | Virtual dispatch support, static init chains, new/delete |
ANSI / ANSI++ | Often not | Only if you use the C library — directly, or transitively through the allocator |
MacTraps2 | Only if needed | Less common trap sets, kept separate deliberately |
And a diagnostic rule worth internalising: on 68K, undefined Toolbox symbols are almost never missing trap calls. Traps are inlined opcodes and cannot go undefined. If the linker names thePort, screenBits, qd, or similar, the problem is the A5 world, and the fix is MacTraps. If it names printf, malloc or __throw, something reached the C library — and if that was unintentional, the allocator is the first place to look.
The measurement that motivates the exercise: on a program with 3170 bytes of its own object code, the difference between the model’s default library set and the minimal correct one was 41098 versus 12602 bytes. On a machine with 8 MB of RAM, that ratio is not an academic concern.
