Diese Seite sammelt, was beim Schreiben und Bauen von C für den klassischen Macintosh wirklich schiefgeht. Sie ist kein Tutorial und kein Ersatz für Inside Macintosh — sie ist die Liste der Stellen, an denen die Dokumentation stimmt und man trotzdem hineinläuft.

Jede Regel hier ist aus einem konkreten Fehlschlag abgeleitet und mit Datum belegt. Das ist Absicht: eine Regel ohne Herkunft lässt sich nicht widerlegen, und unbelegte Regeln halten sich hartnäckig. In diesem Projekt galten vier davon zwischen 82 und 110 Tagen, bevor jemand das Experiment machte, das sie umwarf — sie waren alle falsch.

Was diese Seite besitzt, und was nicht

Sie besitzt das Handwerk: wie man übersetzt, linkt, Ressourcen anhängt, Dateitypen setzt, QuickDraw initialisiert und Ereignisse abholt.

Sie besitzt nicht den Betrieb einer bestimmten Werkzeugkette, nicht den Stand eines Projekts und keine Entscheidungen. Wer mitten in einem Build steckt, braucht etwas, das er durchsuchen kann — dafür ist eine Webseite das falsche Medium. Diese Seite ist die Lehrfassung.


Zwei Werkzeugketten, eine Entscheidung

MPWTHINK C / Symantec C++
BedienungKommandozeile, skriptbarIDE, projektbasiert
Automatisierbarvollständig, über Apple Events an ToolServerüber die KAHL-Suite, Build-Auslösung nicht in allen Versionen belegt
Ausgabe lesbarja, über ToolServerüber die IDE
Baut GUI-Anwendungenjaja, komfortabler
Baut MPW-Toolsjanein

Faustregel. Wer den Build fernsteuern will, nimmt MPW: SC, Asm, ILink und Rez sind Programme mit Ein- und Ausgabe, und ihr Verhalten ist reproduzierbar. Wer eine Anwendung mit Oberfläche entwickelt und dabei am Rechner sitzt, ist mit THINK C schneller — die IDE nimmt einem Projektverwaltung, Ressourcen und den Debugger ab.


Die MPW-Baukette

flowchart LR
  SRC["Quelle .c"] --> SC["SC -model far"]
  SC --> OBJ["Objekt .o"]
  OBJ --> LINK["ILink -model far"]
  LINK --> APP["Anwendung"]
  REZ["Ressourcen .r"] --> RZ["Rez -a"]
  RZ --> APP
  APP --> SF["SetFile -t APPL -c CREA"]
  SF --> RUN["lauffaehig"]

Vier Schritte, und jeder einzelne hat eine Falle, die stillschweigend Erfolg meldet.

-model far gehört auch an den Compiler

Der 68K-Nahmodus adressiert PC-relativ mit 16 Bit — also ±32 KB. Übersteigt das gesammelte CODE diese Reichweite, bricht der Linker mit Error 48: PC-relative edit, offset out of range ab.

Der verbreitete Irrtum: es reiche, -model far beim Linken anzugeben. Tut es nicht. Der Compiler erzeugt die Sprünge, also muss jede Übersetzungseinheit mit -model far gebaut werden.

SC -i :include: -model far :src:main.c -o :obj:main.c.o

Kleine Werkzeuge linkt Link problemlos. Ein Programm um die 100 KB in einem Segment nicht mehr — dort ist ILink das richtige Werkzeug. Es erzeugt eine etwas größere Binärdatei und eine .NJ-Datei für inkrementelles Linken; das ist der Preis.

Set reicht nicht, es muss Export sein

Set LIBS 'MeinMac:Interfaces&Libraries:Libraries:'
Export LIBS

Ohne Export ist die Variable nur in der aktuellen Shell sichtbar. Läuft der Link in einem separaten Skript — und das tut er, sobald man Make > BuildIt; BuildIt benutzt —, expandiert {LIBS} dort zu nichts. Die Bibliothekspfade werden dann zu CLibraries:StdCLib.o, der Linker findet nichts, und die Fehlermeldung spricht von einer fehlenden Datei statt von einer leeren Variablen.

Der Linker schreibt seine Ausgabe auch, wenn er scheitert

Das ist die teuerste Zeile auf dieser Seite.

Ein fehlgeschlagener Link hinterlässt trotzdem eine Datei an der Zielposition — mit plausibler Größe. Exists beweist danach gar nichts.

Zweimal wurde hier ein gescheiterter Build auf diese Weise „verifiziert". Der einzige verlässliche Ablauf:

Delete -i :bin:MeineApp :link.err
ILink … -o :bin:MeineApp ≥ :link.err
Catenate :link.err

Erst löschen, dann bauen, dann die Fehlerdatei lesen — und zwar in einem eigenen Kommando. Ein Verräter am Rande: bleibt der Creator nach dem Link ????, ist die Kette vor SetFile gestorben.

stderr umleiten: , niemals 2>&1

2>&1 bringt die MPW-Shell zum Absturz. Das Zeichen für die Umleitung des Fehlerkanals ist (MacRoman 0xB3), zum Anhängen ≥≥.

Und: Umleiten und Lesen dürfen nicht in derselben Zeile stehen. Wer SC x.c ≥ e.err ; Catenate e.err schickt, bekommt eine leere Ausgabe — die Datei ist zum Zeitpunkt des Lesens noch nicht geschlossen. Zwei Kommandos, immer.

Der Link erzeugt die CODE-Ressourcen und sonst nichts. Alles Weitere — SIZE, vers, Menüs, Dialoge — kommt aus Rez und wird bei jedem Link weggeworfen.

Besonders schmerzhaft ist der Verlust der SIZE-Ressource: ohne das Flag isHighLevelEventAware beantwortet die Anwendung keine Apple Events und jeder Zustellversuch scheitert mit -903 (noPortErr).

Ein leerer Datenfork ist normal

Bei einer 68K-Anwendung liegt der ausführbare Code in CODE-Ressourcen, also im Ressourcenfork. Ein Datenfork der Länge 0 ist der Normalfall und kein Hinweis auf einen kaputten Build.

Zur Prüfung taugt DumpFile app -h: dort muss die Resource Fork Length substanziell sein.


Die StdCLib-Falle: sprintf kostet die halbe Standardbibliothek

Gemessen 2026-08-05.

Ein einziges sprintf in einem sonst stdio-freien Programm lässt den Link mit einer Seite undefinierter Symbole sterben:

# Undefined entry, name: "_flsbuf"   Referenced from "_doprnt" in file "StdCLib.o"
# Undefined entry, name: "write"     Referenced from "fwrite"  in file "StdCLib.o"
# Undefined entry, name: "errno"
# Undefined entry, name: "MacOSErr"

sprintf zieht _doprnt nach, _doprnt die Stream-Maschinerie, und die verlangt die Geräteschicht, die in einem Programm ohne stdio nicht gelinkt ist.

Die Abhilfe ist, Zeichenketten von Hand zu bauen. Zwei Hilfsfunktionen genügen für alles, was ein Programm typischerweise ausgeben muss:

static char *PutStr(char *p, const char *s)
{
    while (*s) *p++ = *s++;
    return p;
}

static char *PutDec(char *p, long v)
{
    char  tmp[12];
    short i = 0;
    if (v < 0) { *p++ = '-'; v = -v; }
    do { tmp[i++] = (char)('0' + (v % 10)); v /= 10; } while (v);
    while (i > 0) *p++ = tmp[--i];
    return p;
}

Jede gibt den Zeiger hinter das Geschriebene zurück, lässt sich also verketten. Das ist weniger bequem als sprintf und spart mehrere Kilobyte gelinkten Code — bei einem Programm, das in ein 68K-Segment passen muss, ist das kein Detail.

Nebenbefund derselben Sitzung: Eine Wortzählung wie grep -c sprintf ist kein Beleg. Sie zählte drei Treffer, alle in Kommentaren, die erklärten, warum der Code sprintf-frei ist.


Header: Typen müssen vor der Deklaration bekannt sein

Gemessen 2026-08-05.

#Error: 'FSSpec' is an unknown or incomplete type
#Error: 'FindAppFileBySignature' previously declared as something else

Die zweite Zeile ist die verwirrende: weil der Typ unbekannt war, hat der Compiler den Prototypen als variadisch verstanden — und meldet an der Definition einen Widerspruch, dessen Ursache Dutzende Zeilen früher liegt.

In einem eigenen Header, der Toolbox-Typen benutzt, gehört der passende Systemheader nach oben<Files.h> für FSSpec, <Events.h> für EventRecord, <Quickdraw.h> für Rect und GrafPtr.


QuickDraw initialisieren

QDGlobals qd;   /* nur bei CFM/PowerPC selbst definieren */

void Init(void)
{
    InitGraf(&thePort);      /* NICHT &qd.thePort */
    InitFonts();
    InitWindows();
    InitMenus();
    TEInit();
    InitDialogs(NULL);
    InitCursor();
}

InitGraf erwartet einen Zeiger auf das letzte Feld der QuickDraw-Globals — bei 68K-Code mit A5-Welt ist das &thePort. Die Variante &qd.thePort stammt aus PowerPC-/CFM-Beispielen, wo es keine A5-Welt gibt und man sich die Globals selbst anlegt. Auf 68K führt sie zu Zeigern, die ins Leere greifen.


Die Ereignisschleife: abgeben ist Pflicht

Das kooperative Multitasking von System 7 verteilt Rechenzeit nur, wenn ein Programm sie abgibt. Der Punkt, an dem das geschieht, ist WaitNextEvent.

while (!gDone) {
    EventRecord ev;
    if (WaitNextEvent(everyEvent, &ev, 2L, NULL)) {
        switch (ev.what) {
            case keyDown:        HandleKey(&ev);   break;
            case mouseDown:      HandleClick(&ev); break;
            case updateEvt:      Redraw((WindowPtr)ev.message); break;
            case kHighLevelEvent: AEProcessAppleEvent(&ev); break;
        }
    }
}

Drei Konsequenzen, die man in dieser Reihenfolge lernt:

Eine Button()-Warteschleife friert die Maschine ein. Sie gibt nie ab, also läuft kein anderes Programm mehr — auch kein Hintergrunddienst, über den man das Programm gerade beobachtet.

GetNextEvent genügt nicht für Apple Events. Wer Apple Events beantworten will, braucht WaitNextEvent und das SIZE-Flag isHighLevelEventAware.

Die Schlafzeit ist ein Angebot, keine Verzögerung. 2L bedeutet: gib bis zu zwei Ticks ab, wenn nichts anliegt. Ein Programm, das rechnen muss, ruft trotzdem in jeder Runde auf.


Ereignisse einspeisen: die teuerste Messung

Gemessen 2026-08-05.

Wer Tastendrücke oder Klicks programmatisch erzeugt, benutzt PostEvent oder PPostEvent. Dabei gilt eine Einschränkung, die in der Dokumentation steht und trotzdem regelmäßig übersehen wird:

SysEvtMask (0x0144) entscheidet, welche Ereignistypen PostEvent überhaupt annimmt. System 7 liefert die Maske mit abgeschaltetem keyUp aus (0xFFEF — alle Bits außer Bit 4), weil kaum eine Anwendung keyUp-Ereignisse auswertet.

Ein PostEvent(keyUp, …) gibt deshalb immer evtNotEnb (1) zurück. Wer das in einer Wiederholungsschleife behandelt, wartet vergeblich:

vorhernachher
ein Tastendruck1,694 s0,121 s
neun Zeichen15,0 s (Zeitüberschreitung)0,825 s

Die Reparatur besteht aus zwei Teilen, und der zweite ist der allgemeine:

short   bit     = (short)(1 << what);
short   saved   = *(volatile short *)0x0144L;
Boolean patched = (Boolean)((saved & bit) == 0);

if (patched) SetEventMask((short)(saved | bit));
for (tries = 0; tries < 48; tries++) {
    e = PPostEvent((EventKind)what, (unsigned long)msg, &qEl);
    if (e == noErr) break;
    if (e == evtNotEnb) break;      /* ein abgeschalteter Typ bleibt abgeschaltet */
    SystemTask();
    Delay(2L, &ticks);
}
if (patched) SetEventMask(saved);

Die Maske ist global — sie muss zurückgesetzt werden. Eine Reparatur, die stillschweigend das Verhalten jeder anderen Anwendung ändert, ist keine.

Und eine Wiederholungsschleife muss wissen, welche Fehler sie beheben kann. evtNotEnb ist eine Aussage über Konfiguration, nicht über Andrang. Ihn zu wiederholen verwandelt eine Ablehnung in eine Verzögerung — und von außen ist das nicht von einer langsamen Maschine zu unterscheiden. Genau daher stammten drei falsche Ursachenzuschreibungen, bevor jemand den Fehlercode auslas.


Pfade nicht annehmen, sondern finden

Gemessen 2026-08-05.

Zwei Fälle, dieselbe Lehre.

Der Systemordner heißt nicht überall „System Folder". Auf einem deutschen System 7 heißt er Systemordner, auf einem französischen Dossier Système. Wer den englischen Namen fest verdrahtet, bekommt auf diesen Maschinen keine Fehlermeldung — sondern eine Datei, die es „nicht gibt".

Zuverlässig ist die Erkennung am Inhalt: der Systemordner ist das Wurzelverzeichnis, das eine Datei namens System enthält. Dieser Name ist in jeder Lokalisierung gleich.

Eine Anwendung findet man über ihren Creator, nicht über ihren Pfad. Die Desktop-Datenbank bildet Creator-Signaturen auf Programmdateien ab — dieselbe Zuordnung, die der Finder benutzt, wenn ein doppelgeklicktes Dokument das richtige Programm öffnet:

DTPBRec dt;
dt.ioNamePtr  = volName;
dt.ioVRefNum  = vRefNum;
dt.ioDTRefNum = 0;
if (PBDTGetPath(&dt) == noErr) {
    dt.ioNamePtr     = spec->name;
    dt.ioFileCreator = 'MPSX';        /* ToolServer */
    dt.ioIndex       = 0;             /* höchste Version */
    if (PBDTGetAPPLSync(&dt) == noErr) {
        spec->vRefNum = vRefNum;
        spec->parID   = dt.ioAPPLParID;
    }
}

Über alle eingehängten Volumes gelaufen, findet das ein Programm unabhängig davon, wohin jemand es verschoben hat.


Kodierung und Dateitypen

Der klassische Macintosh benutzt MacRoman und CR (0x0D) als Zeilenende. Wer Quelltext von einem modernen System herüberreicht, muss beides umwandeln — sonst passieren Dinge, die aussehen wie Compilerfehler und keine sind.

ZeichenZweck in MPWMacRomanUTF-8
Zeilenfortsetzung, Escape0xB6e2 88 82
ƒOrdnertrenner in Makefiles0xC4c6 92
stderr umleiten0xB3e2 89 a5
Platzhalter0xC5e2 89 88

SC verträgt LF, Asm nicht. Der Assembler erzeugt aus einer Datei mit LF-Zeilenenden ein sechs Byte großes Objekt — ohne Fehlermeldung. Der Link scheitert danach an etwas ganz anderem, und niemand denkt an das Zeilenende.

Jede Quelldatei braucht Typ und Creator. Ohne sie meldet der Compiler -31001 („not a text file"):

SetFile -t TEXT -c 'MPS ' :src:main.c

Der Fehler tritt typischerweise nach einem Kopiervorgang aus einem gemeinsamen Ordner auf, weil der die Metadaten nicht überträgt.


Die Meta-Regel: am Artefakt prüfen, nicht am Status

Alle Fallen dieser Seite haben eine gemeinsame Form. Etwas meldet Erfolg und meint etwas anderes:

  • der Linker schreibt seine Ausgabe auch beim Scheitern → Exists beweist nichts
  • der Assembler erzeugt ein 6-Byte-Objekt statt einer Fehlermeldung
  • PostEvent lehnt ab, und die Wiederholungsschleife macht daraus eine Wartezeit
  • eine leere Helferliste ist von einer absichtlichen nicht zu unterscheiden

Daraus folgt eine Arbeitsweise, die anfangs umständlich wirkt und sich sofort auszahlt:

Prüfe das Ergebnis, nicht die Meldung. Existiert die Datei — und hat sie den richtigen Typ, den richtigen Creator, eine plausible Größe und die erwarteten Ressourcen? Ist der Prozess wirklich verschwunden? Steht die Zahl wirklich im Fenster?

Und die Ergänzung, die einen ganzen Tag gekostet hat:

Eine arithmetische Übereinstimmung ist ein Beleg über eine Größenordnung, nie über einen Mechanismus. Eine Rechnung, die eine Messung auf ein halbes Prozent trifft, kann trotzdem auf die falsche Ursache zeigen. Vierzig Zeilen Code, die den Wert auslesen, schlagen einen Abend des Herleitens.


Kurzreferenz

# Übersetzen (jede Einheit mit -model far)
SC -i :include: -model far :src:main.c -o :obj:main.c.o ≥ :cc.err

# Linken (vorher löschen; ILink ab ~100 KB)
Set LIBS 'MeinMac:Interfaces&Libraries:Libraries:'
Export LIBS
Delete -i :bin:MeineApp :link.err
ILink -model far -o :bin:MeineApp :obj:main.c.o ∂
      "{LIBS}CLibraries:StdCLib.o" ∂
      "{LIBS}Libraries:Interface.o" ∂
      "{LIBS}Libraries:MacRuntime.o" ≥ :link.err

# Ressourcen anhängen (nach JEDEM Link)
Rez MeineApp.r -a -o :bin:MeineApp ≥ :rez.err
Rez vers.r     -a -o :bin:MeineApp ≥≥ :rez.err

# Typ und Creator setzen
SetFile -t APPL -c 'MyAp' :bin:MeineApp

# Prüfen — die Fehlerdateien in EIGENEN Kommandos lesen
Catenate :cc.err
Catenate :link.err
Files -l :bin:MeineApp

Diese Seite wird fortgeschrieben. Jeder neue Eintrag bringt sein Datum mit — und wenn eine Regel widerlegt wird, bleibt sie mit einem datierten Vermerk stehen, statt still zu verschwinden. Ein Irrtum, den man nachvollziehen kann, ist mehr wert als eine Korrektur, die so tut, als hätte es ihn nie gegeben.

Quellen

  1. Inside Macintosh: Toolbox Essentials (Event Manager, Menu Manager)
  2. Inside Macintosh: Files (Desktop Manager, PBDTGetAPPL)
  3. MPW 3.5 Command Reference (SC, ILink, Rez, SetFile)
  4. Symantec C++ 8 für Macintosh — THINK Project Manager