From portable functions to portable state machines.
November 2025
Write a compression algorithm in C and it runs everywhere - your phone, laptop, embedded controllers, supercomputers. The same quicksort, the same SHA-256, the same JSON parser. Write once, compile everywhere.
Now write a simple C program that reads a file and sends it over a network:
Your clean algorithm drowns in #ifdef directives. Platform-specific code
everywhere. POSIX tried to standardize this mess, but it was a political
solution to a technical problem. Windows ignored it. Embedded systems can't
support it. WebAssembly doesn't implement it.
Freestanding C libraries are universal. C programs are not.
C succeeded because it has mechanical sympathy - its abstractions align
with how processors actually work. C models the fundamental computational
machine: ALU for arithmetic, program counter for control flow, load/store for
memory. When you write x = y + z, there's no hidden complexity. This
mechanical sympathy makes C portable across every ISA - x86, ARM, RISC-V, MIPS,
whatever ancient thing you dig up, whatever comes next. They all share this
basic computational model.
But programs do more than compute - they communicate. And C never modeled
communication with the same mechanical sympathy. Instead, C punted with system
calls like read() and write() - escape hatches to the operating system, not
a real model. This created "colored functions" - kernel-space red, user-space
blue. Meanwhile, computational functions work anywhere - the same compressor
runs in kernel or user space.
To achieve the same portability for communication, we need mechanical sympathy with how communication actually works - not how we wish it worked.
What is communication, really? Look at any interactive system. An OS kernel handles interrupts. A web server processes requests. A database responds to queries. They're all the same pattern: state machines processing events. This isn't a design choice - it's the nature of communication. Hardware interrupts arrive asynchronously. Network packets come out of order. Every protocol specification describes state transitions.
To achieve mechanical sympathy for communication, we need abstractions that align with this state machine reality.
Programmers have tried various approaches to handle this reality:
System calls hide it behind synchronous interfaces. read() looks like a
function call but relies on the OS to manage async I/O, buffers, and
interrupts. We're fundamentally "hosted" (and therefore vulnerable).
Callbacks face reality honestly - register a function for when an event fires. But they fragment logic into disconnected pieces. Reading a file then sending it becomes callback hell. The state machine exists but it's implicit and scattered.
Explicit state machines finally achieve mechanical sympathy - they model communication the way it actually happens:
struct connection {
enum { CONNECTING, SENDING, RECEIVING } state;
char *buffer;
int socket;
};
void handle_event(struct connection *conn, event_t event) {
switch (conn->state) {
case CONNECTING: /* ... */ break;
case SENDING: /* ... */ break;
case RECEIVING: /* ... */ break;
}
}
Clear and debuggable, but writing these by hand is mechanical drudgery. You're manually modeling the program counter.
Enter coroutines - functions that suspend and resume:
Response make_request(host, port, data):
socket = connect(host, port) // suspends
send(socket, data) // suspends
response = recv(socket) // suspends
return response
You write sequential code. The compiler generates the state machine. Local variables become state fields. Suspension points become state transitions.
This is mechanical sympathy for communication. Just as functions abstract jumps while preserving computational reality, coroutines abstract state machines while preserving communication's async, stateful reality.
Coroutines are to communication what functions are to computation. Both provide mechanical sympathy while preserving readability. Both let compilers handle the bookkeeping while preserving essential semantics.
This is why async/await conquered modern programming. Python, JavaScript, C#, Rust, C++ - they all converged on coroutines because it's the right abstraction. (Though most got the syntax wrong requiring async/await keywords in sequential code and creating new "colored functions." Go and Zig get it right - the keywords are only visible when you care about concurrency).
Functions didn't just clean up goto - they enabled universal interoperability. Standardized calling conventions and ABIs mean any language can call any other.
We need the same for coroutines and state machines. Standardize how they compile. Define the message-passing interface. Create the communication ABI.
C achieved portability through mechanical sympathy with processors. We'll achieve it through mechanical sympathy with async I/O - and everyone already converged on the same pattern:
They're all implementing submit work -> maintain state -> handle completions. From OS kernels to JavaScript's event loop, everyone converged on the same fundamental pattern.
Imagine a standard compilation target for state machines with a universal
message-passing interface. Your protocol implementation compiles to a state
machine that runs on Linux via io_uring, Windows via IOCP, browsers via
WebAssembly, bare metal with no OS at all. The same code, everywhere.
What would this look like concretely? A minimal platform interface:
The runtime is just a scheduler and event dispatcher. State machines become the unit of deployment. PostgreSQL as a library that runs anywhere. Network stacks from microcontrollers to cloud servers. Applications become self-contained - they don't need Linux, just block storage and network I/O.
C conquered computation through mechanical sympathy with processors. It gave us portable libraries. State machines will conquer communication through mechanical sympathy with async reality. They will give us portable programs. Together, they complete the vision of truly portable software. Write once, run anywhere, this time for real.
Both endure because they capture fundamental realities with mechanical sympathy. The computational model survived 50 years of hardware evolution from PDP-11 to modern superscalar processors. A communication model based on state machines and message passing will survive the next 50 years of systems evolution.
This is part one of a series on universal portability. Next: the technical details of a universal async runtime - data structures, interfaces, and implementation strategies that work across all platforms.