3 · The iOS runtime¶
The problem: the translated game is correct C++, but it still thinks it's running on an iPhone in 2012. It calls
malloc, opens BSD sockets, draws with OpenGL ES, asks UIKit for the screen size, reads files from an app bundle. None of those calls mean anything on their own.
The recompiler gives us the game's own code. Everything that code calls into — the C library, the C++ runtime, the Objective-C runtime, and the iOS frameworks — has to be supplied by us. That's the runtime: a portable re-implementation of the slice of iOS that Clash of Clans 1.70 actually uses, plus a thin platform backend that maps it onto whatever host we're running on.
Two halves: external calls in, platform calls out¶
flowchart TB
G["Translated game code<br/><i>(the lifted C++ from chapter 2)</i>"]
G -->|"malloc, socket, glDrawArrays,<br/>objc_msgSend, fopen, …"| STUB
subgraph RUNTIME["runtime"]
STUB["Extern stubs<br/><i>one C++ handler per external function</i>"]
EMU["Emulator core<br/>memory · files · sockets · threads · Obj-C"]
STUB --> EMU
end
EMU --> PLAT
subgraph PLAT["Platform backend (one per host)"]
GL["Graphics<br/>(GL / EAGL / EGL / WebGL2)"]
AU["Audio"]
NET["HTTP"]
UI["Windowing & input"]
end
PLAT --> OS["Host OS<br/>macOS · iOS · Android · browser"]
External calls in: the extern stubs¶
When the original binary called malloc, that was a jump to the system C
library. In our build there is no system C library for ARMv7 — so the lifter
emits the call as a reference to _extern_malloc, and the runtime provides a
handler with that name. At link time the runtime's strong definition wins,
and the call lands in our code.
There are a few hundred of these handlers, grouped by what they emulate:
| Group | Examples | Emulates |
|---|---|---|
| C library | malloc, memcpy, fopen, read, socket |
libc / libSystem |
| C++ runtime | operator new, static guards, COW std::string |
GNU libstdc++ |
| Objective-C | objc_msgSend, class/selector lookup |
the Obj-C runtime |
| Graphics | glDrawArrays, glTexImage2D, … |
OpenGL ES |
| Audio | OpenAL / AudioToolbox entry points | iOS audio |
| UIKit / Foundation | UIScreen, UIDevice, NSURLConnection |
the iOS frameworks |
The C++ runtime is worth a note: the game was built in 2012 with Apple's
llvm-gcc, so it links the GNU C++ standard library (libstdc++) — not the
libc++ that modern Apple toolchains use. You can still see its fingerprints in the
binary: copy-on-write std::string, __gnu_cxx helpers, the gxx set-jump
exception personality. Our re-implementation has to match that older ABI, not
today's one.
Each handler reads the call's arguments out of the guest CPU state, does the real work through the emulator core, and writes the result back — exactly the contract the calling convention defines.
Platform calls out: the backends¶
Some of those handlers need something only the host can provide — a real window, a real GPU, a real audio device, a real network. The runtime keeps those behind small interfaces and links in one concrete implementation per platform:
- Graphics — desktop OpenGL on macOS, EAGL/OpenGL ES on iOS, EGL on Android, WebGL2 in the browser. The game's OpenGL ES calls are forwarded to whichever one is linked.
- Audio — OpenAL + AVAudioPlayer on Apple, OpenSL ES on Android, Web Audio in the browser.
- HTTP —
NSURLSessionon Apple,HttpURLConnection(via JNI) on Android, thefetchAPI in the browser. - Windowing & input — a Cocoa window on macOS, a
UIViewon iOS, the native activity on Android, a<canvas>on the web — all funnelling touches and key presses back into the game.
Choosing the platform is purely a matter of which backend library gets
linked — there are no #ifdefs scattered through the emulator. That's what
makes the same game binary run on five very different hosts.
The emulator core¶
Behind the stubs sits a small operating-system-like core. The pieces worth calling out:
- Memory. The 32-bit game expects a flat 4 GB address space. The runtime
reserves a matching region on the host and hands out a fast allocator on top of
it for the game's
malloc/new. (On 32-bit hosts and the web the mirror is smaller, with guard checks.) - Filesystem. Guest paths are remapped to host paths — game assets resolve to a read-only bundle, while saves and caches go to a writable sandbox directory.
- Networking. The game's BSD socket calls are bridged to real host sockets,
carefully kept inside the file-descriptor range the game's own
select()loop can handle. - Threads. The game is multi-threaded; each guest thread gets a real host thread, its own CPU state, and its own guest stack.
- Objective-C. A minimal Objective-C runtime resolves
objc_msgSendby walking the class/method tables the original binary still carries, then tail- calls into the (lifted) method implementation.
Who owns the main thread?¶
A subtlety that shapes the whole design: on a desktop or in headless CI, our
code owns main() and drives the render loop. But on iOS, Android, and the web,
the host platform owns the main thread (UIKit, the Android activity, the
browser event loop). On those platforms the game runs on a background thread and
the platform pumps it.
The render loop is also where asynchronous work is stitched back into the single-threaded game. HTTP requests, for example, run on worker threads, but their completion callbacks are replayed on the game's own thread once per frame — so from the game's point of view, the 2012 iOS networking model still holds.
sequenceDiagram
participant G as Game (guest thread)
participant R as Runtime
participant W as Worker thread
participant H as Host (NSURLSession / fetch)
G->>R: NSURLConnection start
R->>W: hand off request
W->>H: real network call
H-->>W: response bytes
Note over R,G: next frame
R->>G: replay didReceiveData / didFinish on the game thread
What this layer produces¶
With the runtime linked in, the translated game finally has an iPhone-shaped world to live in: it boots, renders, reads its assets, plays sound, and opens network connections. The last missing piece is something on the other end of those connections — a server. That's chapter 4.