The installer note recorded Open Transport as a hard launch dependency, and the transport-alternatives note named an OT → MacTCP → serial → native fallback ladder. This piece asks the operational question those two raise: what does it actually cost to widen the daemon’s transport from OT-only to MacTCP, and maybe beyond? The answer is encouraging in the place one expects pain and sobering in places one does not. Captured as a design note on branch AppleBridge_mcp; not implemented.
Widening means an abstraction, not a backend
The phrase “add MacTCP support” sounds like writing a second implementation next to the first. It is not. AppleBridge has exactly one transport today, Open Transport, and the code does not so much use it as assume it — the OT handle and OT’s error codes are threaded through the whole daemon. Widening the transport therefore means first introducing a transport abstraction the daemon can talk to without naming OT, and only then writing a second implementor behind it. The cost of widening is dominated by that abstraction — its design, and how much OT-shaped code has to be pried off the rest of the daemon — far more than by the second backend itself.
A framing reminder from the earlier pieces keeps this honest: this is the guest-stack layer (Open Transport, MacTCP, the Serial Manager), not the host-side Basilisk Ethernet backend (etherhelper, slirp, VDE) of the slirp benchmark. Those are independent. This note is entirely about the stack running inside the emulated Mac.
The seam today: a narrow waist with wide leaks
The good news is that a seam already exists. Everything above the transport calls just six functions:
OSStatus InitializeNetwork(void);
void ShutdownNetwork(void);
OSStatus ConnectToHost(EndpointRef *endpoint, unsigned long hostIP, InetPort port);
OSStatus ReceiveData(EndpointRef endpoint, char *buffer, long bufferSize, long *bytesReceived);
OSStatus SendData(EndpointRef endpoint, const char *data, long dataSize);
unsigned long ParseIPAddress(const char *ipStr);
The bad news is that the seam leaks. Open Transport types and constants escape through it into the rest of the daemon:
EndpointRef— the OT connection handle — is not encapsulated. It is declared inmain, passed intoProcessRequest,TopUpCommand, and every verb sender, and the connection is even torn down outside the seam:OTCloseProvider(endpoint)is called directly from the main loop in four places.kOTNoDataErr(−3162) is the load-bearing “no data yet” sentinel. The idle poll and the length-framed reassembly loop both branch on this exact OT error value.InetPort,InetAddress,OTFlags, andParseIPAddressitself are all IP-specific — they presuppose a dotted-quad address and a TCP port.
So the first, unavoidable impact is preparatory: before a second transport can exist, the seam must be tightened. The EndpointRef becomes an opaque ABConn *; teardown moves behind an ABClose(conn); kOTNoDataErr becomes a transport-neutral kABWouldBlock; the address parameters become a neutral endpoint descriptor that can carry a dotted-quad or a serial port name. None of that adds a feature. It is the entry fee.
The hardening tax is mostly already paid
Here is the result that inverts the intuition. The daemon’s hard-won robustness — the three freezes it took to make a blocking socket yield, the application-level heartbeat — looks like deeply OT-specific work that a second transport would have to re-earn from scratch. Most of it would not, because of how those problems were forced to be solved.
Sort the hardening into what is OS-level, what is application-level, and what is genuinely transport-level:
- Yielding is OS-level, and ports for free. The cure for the busy-spin freeze was
WaitNextEventwith a sleep value instead ofGetNextEvent+SystemTask. That is a property of System 7’s cooperative scheduler, not of Open Transport. Any transport’s poll loop reuses it unchanged. - Liveness is application-level, and ports for free. Steady-state dead-peer detection is a timed PING/PONG plus a silence watchdog: the host pings on idle, the daemon ages a
gLastRX/gLastTXtimestamp and declares the link dead after ~30 s of silence. Crucially, this was deliberately decoupled from TCP teardown — the daemon never trusts a FIN or RST, because the MACNAT path may never deliver one. A mechanism that already refuses to depend on connection-state survives the move to a connectionless transport (serial) essentially untouched. The hardest-won piece of robustness is the most portable. - Only three things are genuinely transport-level, and all three live inside the six functions: the connect-completion state machine (OT’s interrupt-time notifier setting a
volatileflag, polled untilT_CONNECT/T_DISCONNECT), the would-block sentinel (kOTNoDataErr), and the flow-control send-retry (kOTFlowErr). These are what a second backend re-implements — and nothing else.
The impact, then: the per-transport cost is narrow and contained, not pervasive. The freeze-proofing does not multiply across transports; it was paid once, at the right layers.
Semantic width: where it stays cheap, where it jumps
How expensive each new transport is depends on how many of three properties it shares with Open Transport.
| Property | Open Transport | MacTCP | Serial | Native channel |
|---|---|---|---|---|
| Addressing | IPv4 + port | IPv4 + port (same) | port name + baud (none) | host-defined |
| Connection model | connected stream | connected stream (same) | no connect phase | host-defined |
| Async / would-block idiom | notifier + kOTNoDataErr | ASR (notifyProc) + ioResult>0 | SerGetBuf byte count | host-defined |
| Re-uses heartbeat + yield | — | yes | yes | yes |
MacTCP is the narrowest possible widening. It preserves both addressing and the connection model — it is still “open a TCP stream to host:port” — so the neutral endpoint descriptor, the connect concept, and the reconnect loop carry over without conceptual change. Only the async idiom differs: MacTCP drives asynchrony through Device Manager parameter blocks (PBControl on a TCPiopb), an Asynchronous Notification Routine in notifyProc, and an ioResult that stays positive while a call is incomplete — a different spelling of the same state machine the OT backend already implements. One file’s worth of new code behind a seam that does not have to grow.
Serial is a genuine jump, because it breaks two of the three properties. It has no IP address (the descriptor must carry a port name and baud rate), and no connection phase at all — which is a double-edged impact: it removes the async-connect freeze hazard entirely (there is no handshake to block on), but it also removes any connection-state liveness signal. The latter costs nothing only because the heartbeat already never used one. Serial forces the abstraction’s address and connect concepts to widen beyond their TCP shape; it is the rung that tests whether the seam was designed well.
A native paravirtual channel is different again, and out of scope here — it changes the topology rather than the stack, and means forking the emulator.
Widening the daemon widens the host
One impact is easy to miss because it lives on the other machine. host/host_server.py is a TCP server: the daemon dials out to it. MacTCP keeps that exactly — it is still a TCP client connecting to the same listener, so the host side does not change at all. But serial or a native channel change the topology: there is no socket to accept. The host would have to mirror the new transport — read a pseudo-terminal for serial, or service a paravirtual channel — which means the transport abstraction is not a Mac-only concern. Up to MacTCP, widening is one-sided; beyond it, it is two-sided.
Operational impact
Two backends mean the daemon must choose one, and there is no notion of transport selection anywhere today. The connect path hardcodes ParseIPAddress(gPrefs.ip) → ConnectToHost(…, BRIDGE_PORT), and the prefs schema knows only IP=, DEBUG=, and APP=. Adding a TRANSPORT= key touches a predictable set of places: the prefs struct and its parser/writer; the connect dispatch in main; the config app and the control panel UI that surface the choice; and network.c, which splits into per-transport back-ends behind the seam. Selection at runtime is the installer’s capability ladder — probe gestaltOpenTpt, else MacTCP, else serial — relocated from install time to daemon start.
The quieter, recurring cost is the test matrix. Correctness must now be demonstrated across transports × host types (Basilisk II, SheepShaver, real 68k), and the rarely-exercised path is the one that silently bit-rots. That ongoing verification burden, not the initial port, is the durable price of carrying more than one transport.
Reach versus the footprint ceiling
What does the widening buy? Real reach: MacTCP runs on 68000 and 68020 machines — the Plus, SE, Classic, LC, the original Mac II — and on System 7.0/7.1, none of which Open Transport can serve (OT wants a 68030 or better and several megabytes of RAM). On paper, MacTCP roughly doubles the set of Macs that could host the bridge.
The sobering caveat ties straight back to the installer note: the daemon declares a 12 MB-preferred / 8 MB-minimum partition, and the very low-end Macs MacTCP reaches top out near 4 MB of total RAM. So widening the stack does not, by itself, widen the hardware reach — the memory footprint gates it. The immediate, real beneficiary of MacTCP is therefore an OT-less but well-resourced machine — say a 68040 on System 7.1 that simply never had Open Transport installed — not literally a Mac Plus, until the daemon’s footprint is cut. The transport widening and a memory diet are complementary; one without the other only half-delivers the reach.
Verdict: tighten the seam, then add MacTCP, and stop there for now
flowchart TB
SEAM["Tighten the seam
opaque ABConn, kABWouldBlock,
teardown behind ABClose,
neutral endpoint descriptor"]
SEAM --> OT["OT backend
(existing, unchanged behaviour)"]
SEAM --> MACTCP["MacTCP backend
same addressing + connection model
only the async idiom differs"]
SEAM -.defer.-> SERIAL["Serial backend
widens address + connect concepts
two-sided host change"]
SEAM -.separate project.-> NATIVE["Native channel
fork the emulator"]
MACTCP --> REUSE["reuses heartbeat + yield + reconnect
(already transport-agnostic)"]
The phased recommendation follows the costs above:
- Tighten the seam first. Opaque handle, neutral would-block sentinel, teardown behind the waist, neutral endpoint descriptor. This adds no feature but is the prerequisite for everything else — and it makes the existing OT path cleaner regardless.
- Add MacTCP as the first widening. It is the narrowest semantic jump (same addressing, same connection model), the hardening already ports, and the host side is untouched — the largest reach for the least architectural disturbance. It also proves the seam on an easy case before the hard one.
- Defer serial until the abstraction’s address and connect concepts have been widened to a byte-pipe. It pays back the no-network case and even removes the connect-freeze hazard, but it is a two-sided change and should follow, not lead.
- Treat a native channel as a separate project — a different topology and an emulator fork, not a transport backend.
The decision criterion for each rung is the same: does the next transport extend the seam, or break it? MacTCP extends it; serial bends it; the native channel replaces it. Widen in that order and the cost stays close to the surprising lower bound this analysis found — because the expensive part, the freeze-proofing, was already paid at the layers that do not care which transport runs underneath.
Status: design note on branch AppleBridge_mcp; not implemented. The hardened, proven configuration remains Basilisk II with Open Transport over etherhelper. The MacTCP and serial backends scoped here build on the transport-alternatives note and the installer necessity checklist; the seam-tightening they require is the first concrete step and is not yet done.

