Cross-dev from Linux
The cross-compile-clean workflow: Retro68 syntax checks on Linux, then SCP the patch to the Mac for the real CodeWarrior build.
Everything learned about shipping Mac OS 9 code from a Linux desktop, plus the round-trip workflow that produced fixes 100+ through 151. The platform is unusual enough that much of this isn't documented anywhere else — this page is the collected notes.
1. Platform constraints (what runs on the Mac)
Compiler
CodeWarrior 8 Pro (with 8.3 update) on Mac OS 9. Strict C89. Hard restrictions worth tattooing on your forearm:
- No
inlinekeyword (define it away:-Dinline=) - No
//line comments - No variadic macros — but
__VA_ARGS__is supported as a pre-C99 extension and is the safe path for NSLOG-style no-op macros - No forward enum declarations (
enum foo;is illegal) - No C99 designated initializers (
{ .field = value }) - No for-scope variable declarations (
for (int i = ...)) - No compound literals
- No
restrict,long longis technically supported but miscompiled (see §3) - No GNU union casts (
(union_type)0) — use a compound statement with a local variable - All variables at the top of their enclosing block
C89 also rejects named enums inside struct bodies — CW8 silently fails to complete the struct, leaving it incomplete and every member undefined. Hoist named enums above any struct that uses them.
Bit-pack scalar fields, but watch padding
css_computed_style_i (libcss inner struct) gets memcmp'd by arena interning to deduplicate styles. Any new field's byte representation must be deterministic across every cascade path. This means:
- A
uint8_tbetween twoint32_tcreates 3 padding bytes the compiler chooses; cascade code writes the byte but nobody writes the padding. Different code paths can leave the padding with different values → memcmp false-negatives → intern table fills with duplicates → use-after-free during style destroy. Useint32_tfor scalar fields in_iso they self-align. This is fixes117 (inline track array crash) and fixes151b (uint8 field crash) — the same trap twice. - Variable-size data (track arrays, content arrays, font-family lists) goes in the OUTER
css_computed_style, not in_i. Add a dedicatedarena__compare_*function for it. fixes118 documented this pattern; fixes150 reused it for row tracks.
Carbon CFM app requirements
'carb'resource is mandatory. Without it, CarbonLib never loads, and any*InContextOpen Transport call crashes inside OTClientLib at a fixed address. ShipMacSurf.rsrc(a pre-built binary resource fork; CW8 links.rsrcinto the output with no Rez step) and list it in the project.- Skip
InitGraf/InitFonts/InitWindows/InitMenus/TEInit/InitDialogsunder Carbon. KeepInitCursor()andFlushEvents(everyEvent, 0). - Call
RegisterAppearanceClient()afterInitCursor(), gated by a Gestalt check. - No
kWindowStandardHandlerAttributeonCreateNewWindow— it intercepts updateEvts and leaves windows blank.
Open Transport rules
- Use plain
OTOpenEndpoint/InitOpenTransport, not the*InContextvariants. Every CarbonLib crash we've debugged has traced to InContext routing through OTClientLib in an uninitialized state. OTUseSyncIdleEvents(ep, true)plus a notifier that callsYieldToAnyThread()onkOTSyncIdleEvent— this is how synchronous OT calls cooperate with the rest of the app.- Carbon CFM cannot do passive (listening) OT binds. The OS rejects it categorically; we verified across 14 rounds in macTLS before pivoting. Don't try to write a local server.
No preemptive threads
OS 9 is cooperative multitasking. Run a WaitNextEvent UI loop. Long-running work yields via YieldToAnyThread() or by returning to WNE.
Memory partition
Carbon partition must be at least 16 MB preferred / 8 MB minimum. libcss allocates via raw malloc/calloc with no NetSurf wrapper — a 4 MB partition (the CW8 default) starves the CSS cascade on real pages. Set via MWProject_PPC_size / MWProject_PPC_minsize in the .mcp XML.
CW8 PPC long-long codegen is broken
(long long)a * small_const writes a >> log2(const) into the high word instead of (a * const) >> 32. Confirmed on real hardware via probe G (fixes113): (long long)131072 * 1024LL produced hi=128, lo=134217728 instead of the correct full product 549,890,031,616.
Mitigation: for any 64-bit fixed-point math, route through double under #ifdef __MWERKS__. PPC has a hardware FPU and IEEE 754's 52-bit mantissa covers every int32 fixed-point intermediate. See browser/netsurf/include/libcss/fpmath.h for the reference pattern.
Pure int32 multiplies and divides are fine. The miscompile is specifically the 64-bit shift-multiply path.
Carbon event class availability
Mac OS X Carbon events that were never back-ported to CarbonLib (e.g. kEventMouseWheelMoved) will register without error but never dispatch, and CarbonLib's dispatcher destabilizes when something downstream tries to deliver an event whose class it doesn't know. Symptoms: illegal-instruction crash at a fixed-looking address inside CarbonLib.
Check Apple's CarbonEvents.h annotations before registering any handler. If CarbonLib: not available, the platform cannot deliver that event — don't install a handler, don't debug your handler, accept the platform constraint.
Live-tracking scroll bar CDEF is hardware-specific
kControlScrollBarLiveProc = 386 crashes on real G3/G4 hardware (not reproducible in SheepShaver). Use kControlScrollBarProc = 384 (non-live) instead. Trade-off: thumb drag doesn't scroll until mouseup. fixes159 documented this.
UPP macro override on CarbonLib is unsafe
CarbonLib's TrackControl / InstallEventHandler expect MixedMode RoutineDescriptor UPPs, not raw PPC function pointers. The "Carbon UPPs are just native function pointers" shortcut only works on Mac OS X's Mach-O Carbon.framework, not CarbonLib on OS 9. Symptom: PC in very low memory (e.g. 0x00000008) with CurApName showing CodeWarrio.... Either let CW8's Universal Interfaces expand the macro normally, or use the TrackControl(ctrl, pt, NULL) no-callback path and read state via GetControlValue() on return.
2. CW8 header and access path quirks
Suppress Carbon.h sub-headers by pre-defining their guards
Carbon.h transitively pulls in InternetConfig.h, MacWindows.h, KeychainCore.h, ATSLayoutTypes.h, SFNTLayoutTypes.h — each of which has at least one C89 incompatibility or missing forward declaration. The clean workaround is to #define __INTERNETCONFIG__ (or whatever Universal Interfaces guard) before #include <Carbon.h>. Carbon.h's own guard check sees the symbol defined and skips the include entirely.
Per macos9.h:
#define __INTERNETCONFIG__
#define __KEYCHAINCORE__
#define __ATSLayoutTypes__
#define __ALIASES__
struct AliasRecord;
typedef struct AliasRecord *AliasPtr;
typedef AliasPtr *AliasHandle;
#include <Carbon.h>
The AliasHandle forward decl is needed because MacWindows.h uses AliasHandle in parameter types even with Aliases.h suppressed.
Shim headers must use the same include guard as the real header
If a shim header sets its own guard (e.g. LIBWAPCAPLET_LIBWAPCAPLET_H) but the real library header uses a different guard (e.g. libwapcaplet_h_), CW8's access path will find both in the same TU. The stub processes first, sets its own guard; later in the same TU the real file is found via a different path, its guard is unset, it processes too, and you get "illegal name overloading" on every type both files define.
Always make the stub's guard identical to the real header's guard. Applied in fixes264 for libwapcaplet/libwapcaplet.h and css/utils.h.
CW8 can't resolve relative includes from headers found via access paths
When a header found via an access path does #include "subdir/foo.h", CW8 can't find foo.h because the local-directory context is lost. Simple basenames (#include "foo.h") work if the target is in the same directory. Path prefixes (#include "utils/list.h") fail.
Workarounds: make headers self-contained (inline the content), or use access paths that put the target file in the same search directory.
macsurf_prefix.h's globals
The prefix file gets injected before every TU. It must:
- Include
<MacTypes.h>first (otherwisebool/true/falseconflict with later includes) - Define
__MACOS9__ 1 - Define
NO_IPV6 1 - Define
TARGET_API_MAC_CARBON 1 - Define
MACSURF_DEBUG 1(gated on#ifndef MACSURF_RELEASE) — this is what enables the file-backed debug log; without it the entire log channel compiles to empty stubs (fixes305a regression) - Stub
NSLOGvia#define NSLOG(cat, level, ...) do{}while(0). CW8 supports__VA_ARGS__as a pre-C99 extension. Do not use a varargs C function — it forces the category token (fetch,llcache,layout, etc.) to be evaluated as an expression and fails with'fetch' undeclaredin TUs where those names are not in scope.
Two-source dispatch.c gotcha
libcss/src/select/dispatch.c and libcss/src/select/s_dispatch.c both define prop_dispatch[]. The Mac builds s_dispatch.c (basename match per commit a2f5656d's flat-folder rename); the Linux side has both as a historical accident.
- Only one belongs in MacSurf.mcp. Both = duplicate-symbol link error.
- Keep both byte-identical when adding properties. Otherwise CSS_N_PROPERTIES grows but only one table grows with it → out-of-bounds dispatch → unmapped-memory exception (fixes116b crash signature
0x68F168F0). - Ship both files in the fix tar even though only one is built, so they don't silently drift.
int * and int32_t * are incompatible on CW8 PPC
On PPC, int32_t = long (not int). CW8 is strict about pointer types: passing int * where int32_t * is expected fails with "illegal implicit conversion from 'long *' to 'int *'". Common trap: autogenerated libcss parse files have int32_t *ctx, but a shared utility declared int *ctx. Make all signatures consistent.
Forward enum declarations are illegal
Any C99 header that uses enum foo; to forward-declare an enum will fail on CW8 with "undefined identifier 'foo'". Replace with the actual #include of the header that provides the full enum.
3. Workflow: Linux → tar → scp → Mac
Repo layout
~/Webs/macsurf/ primary working tree (Linux git)
~/Documents/macfiles/ outbound tar staging
fixes149.tar, fixes150.tar... one tar per shipped round
Old Zips/ archived once a round is closed
Per-round process
-
Edit on Linux in the macsurf working tree.
-
Pre-flight on Linux with Retro68 PPC GCC for C89/CW8 compatibility:
/home/patrick/Retro68/toolchain/bin/powerpc-apple-macos-gcc \ -std=c89 -pedantic-errors -Wall -Wno-long-long -Wno-pedantic \ -fsyntax-only -Dinline= -D__MACOS9__ -DCSS_INTERNAL \ -I[various include paths] \ path/to/changed_file.cPre-existing errors from the libcss/lwc shim mismatch are OK and pass on CW8 — what matters is that no NEW errors appear at the edit sites.
-
Bundle as a tar with the full path tree preserved:
tar -cf ~/Documents/macfiles/fixesNN.tar \ browser/libcss/src/parse/properties/p_foo.c \ browser/libcss/src/select/properties/s_foo.c \ [other changed files...]The tar contains only the files changed in this round — never a full source dump (that would clobber the user's local edits to MacSurf.mcp / Access Paths). After the initial bootstrap round, every tar is delta-only.
-
Ship via scp:
scp -P 2222 -i ~/.ssh/macsurf_push \ ~/Documents/macfiles/fixesNN.tar \ patrick@localhost:Documents/macfiles/The user's Mac is reachable via an SSH bridge on localhost port 2222.
-
Mention any new
.cfiles for MacSurf.mcp. Do not edit the.mcpfile from Linux — the user maintains the project file list on the Mac via the CW8 IDE, and a Linux-edited.mcpwould clobber their local state. -
Commit on Linux with a substantive commit message that captures the architectural intent. Commits are local working memory and become CLAUDE.md material when the round closes.
-
User unpacks on the Mac, adds any new
.cfiles to MacSurf.mcp via CW8, rebuilds with "Remove Object Code" first, runs the binary. -
User reports back via screenshots and/or the file-backed log (see §4).
-
Update CLAUDE.md to record what shipped (the "Last shipped fix" entry rolls forward, with predecessors demoted below).
What goes in the tar
Source files only — never .mcp, never Access Paths.xml, never docs. The tar's footprint stays small (tens of KB up to ~300 KB for big rounds with autogenerated headers).
Numbering
Fix numbers are used as-is. Letter suffixes (fixes148b, fixes148b3) mark follow-up attempts within a single feature's iteration when the first shipped version needed more work.
Don't assume stale files
The build is what it is. If something looks like staleness, the answer is to find a real root cause in code — a missing include, a wrong field path, an autogen mismatch — rather than blaming CW8 caches or zip-extraction failures. If a previously-shipped file may need to be re-shipped because symptoms continued, just include it in the next tar with no fanfare.
4. Diagnostics: file-backed log + hardware-only verification
The log channel
browser/netsurf/frontends/macos9/macsurf_debug_log.c writes one CR-terminated line per MS_LOG(msg) or macsurf_debug_log_writef(fmt, ...) call to MacSurf Debug.log on the Desktop, flushing after every write (FlushVol + SetFPos). This is the primary post-crash backtrace channel since we can't attach MacsBug remotely.
- Located via
FindFolder(kOnSystemDisk, kDesktopFolderType, ...). If FindFolder fails the channel is silently inert. - Format specifiers supported:
%d,%ld,%p,%s,%%(and%lxis NOT supported — use%ldand decode hex by eye if you need it). - Output capped at 255 bytes per call.
- Gated on
MACSURF_DEBUGin macsurf_prefix.h. The#ifdef MACSURF_DEBUGtest happens inside the implementation; if the define is missing, every call site compiles to nothing (fixes149/150/151 silent-instrumentation regression, then fixes305a same vector).
Subsystem-init audit checklist
After fixes149's three rounds of silently-failing instrumentation, we adopted this checklist:
- Init function wired in
main.c?grep -c subsystem_init main.cmust be > 0. - Init function body actually reachable?
#ifdef MACSURF_DEBUGand similar feature-macro gates can compile the body to an empty stub.grep -rn "define MACSURF_DEBUG"must turn up at least one site. - Smoke-test confirming the subsystem produces its externally-visible artefact at startup (log file appears, title bar updates, menu item present).
Step 2 is the new one — it catches the fixes305a vector where the init call is present but the macro is missing.
Verifying via hardware probes on advanced.html
Most CSS feature work ships with probe cards added to mactrove.com/advanced.html (served from the Drupal host, not from the Mac, so refreshing the URL is enough — no Mac-side ship). Each fix sprint adds a section with N labelled probe cards (R1-R5, C1-C5, V1-V4, G1-G6, etc.); the user refreshes on the G3 and reports green / red per card.
For positioning verification, the file-backed log captures plot_rect and plot_text calls with exact (x,y) coordinates. Math against expected positions confirms whether a CSS feature is correctly applied — e.g. "R1-3 at y=267, R1-1 at y=179 → diff=88px = 80 (row 0) + 8 (gap) ✓".
SheepShaver is a partial test
/home/patrick/Webs/MAC/sheepshaver/ runs OS 9.0.4 via SheepShaver on Linux, useful for:
- Smoke testing — does the build launch at all, does Carbon init succeed
- Rendering regression checks — does MacTrove render, does var() resolve
- Obvious logic bugs that escape Linux syntax check
It is not a substitute for real hardware on:
- Hardware-specific crashes (wheel events, scroll-bar live-track, USB Overdrive interactions)
- Real network behavior (the prefs as shipped have no usable ethernet, so first fetch hangs ~2 min on timeout)
- Timing-sensitive bugs (JIT/coop-scheduler pacing differs from real PPC)
A SheepShaver "passed" never closes a hardware-side gate.
MacsBug is the last resort
The G3 dev machine has MacsBug installed but the user currently lacks an ADB keyboard, which is required for many MacsBug commands. When a crash is hardware-specific and the Linux-source audit comes up empty (the documented case is the wheel crash: see CLAUDE.md "Wheel crash diagnostic exhaustion"), progress halts until ADB hardware is available for wh/sc/ip/dm sp capture.
5. Known gotchas
Every non-obvious failure mode that took serious debugging time
These are the hard-won lessons — each one represents at least 30 minutes of debugging. The symptom, the root cause, and the reference fix are listed for each:
- Two-source
dispatch.c/s_dispatch.crequiring synchronized updates (fixes116b) uint8_tbetweenint32_tfields creating non-deterministic padding (fixes117 / fixes151b)- CW8 PPC
long longmiscompile (fixes113) - UPP macro override unsafe on CarbonLib (fixes147)
- Live-track scroll CDEF crash on real hardware (fixes159)
kEventMouseWheelMovednot in CarbonLib (fixes140)- Inserting a field mid-struct in
css_computed_style_i— CW8 doesn't recompile every dependent object, so libcss objects keep the old field offsets while the netsurf side uses new ones. Silent crash mid-reformat; move new fields to the end of the struct (fixes159a) TARGET_API_MAC_CARBONdefined too late to matter. It must sit above the prefix file's#include <MacTypes.h>, becauseConditionalMacros.hcomputes the Carbon triplet there and then guards itself. Set it later and you silently get a non-Carbon binary compiled against Carbon headers — survivable on OS 9, fatal on OS X (fixes951)#include <Endian.h>never reaches the SDK header. HFS is case-insensitive,libparserutils/src/utils/endian.his on an access path, and user paths are searched first — so the include silently resolves to the wrong file. Cost one 6,582-error build. Same class as thesys/time.htrap- The bit-packed
bits[16]computed-style array is full. The next native CSS property needs the array extended first, which is a structural change to an arena-interning-sensitive struct — not a normal property add - The release log gate matches a literal whitelist of diagnostic strings, not a
RECONprefix. A brand-new diagnostic line is silently dropped until its exact string is added to the list
6. Reference tree
| Need | File / dir |
|---|---|
| Project state, gotchas, ship history | CLAUDE.md (root) |
| CSS feature inventory | CSS_STATUS.md |
| Architecture (broad) | docs/macsurf-architecture.md |
| Window-framework architecture | docs/research/window-architecture-2026-04-22.md |
| Code | browser/netsurf/ (NetSurf fork) + browser/libcss/ (with MacSurf patches) |
| Mac frontend | browser/netsurf/frontends/macos9/ |
| Debug log impl | browser/netsurf/frontends/macos9/macsurf_debug_log.c |
| Cross-pre-flight toolchain | /home/patrick/Retro68/toolchain/bin/powerpc-apple-macos-gcc |
| SheepShaver test env | /home/patrick/Webs/MAC/sheepshaver/ |
| Hardware probe page | mactrove.com/advanced.html (lives at /home/patrick/Webs/MAC/drupal/web/advanced.html) |
| Outbound tars | ~/Documents/macfiles/ |
| SSH bridge to Mac | scp -P 2222 -i ~/.ssh/macsurf_push patrick@localhost:... |
| Log pull from Mac | scp -P 2222 -i ~/.ssh/macsurf_push patrick@localhost:'Documents/macfiles/MacSurf Debug.log' /tmp/ |