What it is
iWish for iOS 9 is a 32-bit armv7 / iOS 9.3 port of iWish — the AndroWish stack that runs Tcl/Tk on SDL2 with batteries included. It builds a self-contained sdl2wish interpreter plus a set of loadable native extensions, all compiled for 32-bit Apple A-series devices (such as an iPad mini 1, A5 CPU, no Metal) not targeted by current toolchains and app stores.
It is a separate build tree from the arm64 iOS/Catalyst iWish port: the two build trees do not share output, and shared C code is guarded by iOS-version macros rather than forked. Rendering uses SDL2's GLES2 path with AGG software rasterization, because these GPUs predate Metal.
The Decent Espresso de1app runs on top of this port — GUI, Bluetooth LE hardware discovery, and in-app self-update work end-to-end on an A5 iPad.
Scope
No current SDK builds cleanly for armv7, and modern side-loading tools require iOS 12.2 or newer. A first-generation iPad mini cannot run current software.
With a jailbreak for signing, this port provides a Tcl/Tk environment with networking, TLS, Bluetooth, SQLite, and a full widget toolkit, sufficient to run an application. The result is a single-purpose appliance on a decade-old tablet.
Status
- Tcl and Tk regression suites pass on-device. The stack (FreeType, SDL2 GLES2, Tcl 8.6, sdl2tk + AGG, sdl2wish) builds as armv7, minimum iOS 9.0, and runs on the device. Remaining test failures are floating-point edge cases (armv7
long double==double) and known sdl2tk reduced-X11 backend semantics — no armv7 codegen bugs, no crashes. - Bluetooth LE: verified end-to-end on an A5 iPad — scanning delivers advertised names, connect works, GATT discovery works, characteristic reads work.
- de1app runs on the device — GUI renders and BLE discovery of real hardware is confirmed.
- In-app self-update works over HTTPS, after fixing an armv7-specific TLS crypto bug (see below); an HTTP + SHA-256 path remains as a fallback.
- Idle CPU cut ~63% via a batched event-loop pump (see below).
- Batteries: ~130 native packages verified loading on-device, and the demos menu is fully enabled.
Open item for daily use: startup performance — first launch is dominated by Tk photo-image scaling of skin assets. The fix is to pre-scale and cache skin PNGs, or to ship a lighter skin.
Building
The build is driven by shell scripts in the repo's ios9-build/ kit:
- A foundation build compiles FreeType, SDL2 (GLES2, Metal off), Tcl/Tk and sdl2tk into
sdl2wish, targetingarmv7-apple-ios9.0against an iOS 9.3 SDK. - An extensions harness cross-compiles each native battery (tls, sqlite3, tdom, BLT, tkimg, zint, Thread, Itcl, treectrl, and more) into ldid-signable loadable dylibs, with a static verifier confirming every
pkgIndextarget resolves. - An app-bundle step assembles and signs the
.app, usingldidwith a platform-application entitlement set (jailbreak, no App Store).
The prerequisites, tree layout, and step-by-step commands are in ios9-build/BUILD.md.
Implementation notes
armv7 toolchain (a two-compiler build)
Current Apple clang cannot assemble 32-bit armv7 reliably: its integrated assembler fails on Tcl's large bytecode-interpreter function ("relocation not in range," at any optimization level, ARM or Thumb). The build uses a compiler wrapper that compiles with a modern NDK clang (which has a working armv7 assembler) while linking with Apple's linker via the classic-linker flag, which produces a binary that loads on jailbroken iOS 9.
The iOS 9.3 SDK was missing mem*/str* stub symbols, an entire libc++ header set, and several availability headers, so the build idempotently repairs the SDK before compiling, and several SDL source files are guarded so they compile against the old SDK.
TLS on old iOS
The bundled LibreSSL initially produced a wrong TLS 1.3 handshake on armv7. Root cause: the crypto code included the system <endian.h> and called be64toh() for SHA-512/384's 64-bit words, but on iOS that function doesn't exist. Compiled as an implicit int-returning function, every 64-bit value was truncated to 32 bits — so SHA-384/512 were wrong while SHA-256 was correct. Because the TLS 1.3 cipher suite in use derives its keys via HKDF-SHA384, the client computed the wrong handshake keys and could not decrypt the server's first record. In-process test handshakes had masked the bug, since both sides shared the same broken hash.
The fix forces correct 64-bit byte-swap macros into the LibreSSL build. With that in place, on-device known-answer tests pass and a TLS 1.3 handshake to a live server succeeds, enabling HTTPS self-update on the device.
Idle CPU / heartbeat
de1app pegged a CPU core when idle (foreground, screen on). Profiling showed the cost was native, not application-level: sdl2tk's event thread called SDL's SDL_WaitEvent, which on iOS runs a full CFRunLoop pump on every event returned — so a burst of queued events cost one pump each, and a fixed 100 Hz timer heartbeat kept the queue non-empty.
Two changes addressed it: lowering the iOS heartbeat rate, and replacing the wait loop with a batched pump that drains the already-queued events first and only pumps the CFRunLoop when the queue is empty — one pump per drained burst instead of one per event. Foreground-idle dropped from ~0.83 core to ~0.31 core (~63%) on the A5. The same batched-pump change ports to modern arm64 iPads, where it also cuts idle CPU (~70% on an M2 device).
Repository
Source, build scripts, and build notes: https://github.com/johnbuckman/iwish-ios9
This repo is a full-source, clone-and-build snapshot — a fork of AndroWish (Tcl/Tk-on-SDL2 by Christian Werner) with the armv7 / iOS 9.3 source patches and build kit added. Upstream AndroWish, Tcl/Tk, and each bundled extension retain their own licenses.
It has an arm64 sibling for modern iOS devices, iwish.