iWish for iOS 9

The undroidwish extension set built for 32-bit armv7 and iOS 9, for hardware that cannot run current iOS.

iwish-ios9 on GitHub →

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

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:

  1. A foundation build compiles FreeType, SDL2 (GLES2, Metal off), Tcl/Tk and sdl2tk into sdl2wish, targeting armv7-apple-ios9.0 against an iOS 9.3 SDK.
  2. 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 pkgIndex target resolves.
  3. An app-bundle step assembles and signs the .app, using ldid with 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.