Embedded Swift for the iPod Nano 2G, running as Rockbox
plugins. Each example under examples/ compiles to a freestanding ARMv4T
object (the Nano 2G's FPU-less ARM940T has no OS, no libc, and no Swift
runtime beyond what Embedded Swift itself needs) and pairs it with a thin C
host that's the actual Rockbox plugin.
examples/hello-- the minimal case: one@_cdeclfunction, one C caller, proof Swift code executes on-device and can call back into Rockbox. Start here.examples/cube-- a Swift port of Rockbox's ownapps/plugins/cube.cdemo, a rotating wireframe cube. The bigger example: real per-frame math (rotation, projection, backface culling) running in Swift every frame, not just a one-shot call.
common/ shared by every example
plugin/ rockbox_shim.{c,h} (Embedded Swift's runtime floor +
soft-float sinf/cosf) and the CRockbox module.modulemap
source/ Rockbox.swift, the Swift-facing wrapper over the shim
examples/<name>/
plugin/ the example's own C: <name>.c (plugin_start), SOURCES,
<name>.make
source/ the example's own Swift
Makefile sets a few variables, includes mk/common.mk
mk/common.mk the actual build logic (swiftc, audit, archive, Rockbox
sync + link, install) -- shared, not per-example
rockbox/ pinned Rockbox checkout (gitignored, shared build input)
build/<name>/ build output per example (gitignored)
Rockbox plugins are C: the OS calls plugin_start(), and every system
service (display, buttons, files) goes through an rb-> function-pointer
table passed in at load time. Each example's plugin/<name>.c is that
entry point; it calls into the example's Swift (@_cdecl functions) and
uses what comes back -- proof the Swift code actually executed on the
device, not just linked.
The round trip goes both ways. common/plugin/rockbox_shim.h declares the C
surface Swift calls into -- rb_splash (a fixed-arity wrapper around
rb->splash, since Embedded Swift can't call a variadic C function
directly) plus sinf/cosf for examples doing their own math -- and
common/plugin/module.modulemap exposes that header to swiftc's Clang
importer as the CRockbox module. common/source/Rockbox.swift wraps the
Rockbox-specific part of that (not the plain math) in a small Swift-facing
namespace, Rockbox.splash(...), shared by every example. This mirrors the
CRockbox module / Rockbox wrapper split
celeste-swift's ports/iPod
uses for its own (much larger) Rockbox port.
Even a function as trivial as hello's n &+ 42 references a small, fixed
set of runtime symbols Embedded Swift always needs on this target
regardless of what it calls: a memory allocator, a handful of
atomic-refcount primitives (harmless as plain, non-atomic operations here
-- everything Swift touches runs on the plugin's single main thread), a
couple of EABI memory-clear helpers, and a stack-protector pair.
common/plugin/rockbox_shim.c provides all of that -- nothing about any
one example's logic drives its size, it's Embedded Swift's baseline floor
on armv4t-none-none-eabi.
Requires a Swift toolchain with a prebuilt Embedded Swift stdlib for
armv4t-none-none-eabi (swift-6.3.2-RELEASE or newer), and Docker on macOS
(the arm-elf-eabi-gcc 9.5 cross toolchain can't be built with an arm64
Darwin host; on native Linux it builds directly). The Rockbox checkout and
cross toolchain are shared across every example -- building a second
example after the first is much faster.
make list # show available examples
make EXAMPLE=hello toolchain # once, ~40 min (or natively on Linux)
make EXAMPLE=hello rock # -> build/hello/hello.rock
make EXAMPLE=hello install IPOD=/Volumes/<your-ipod>(EXAMPLE defaults to hello; equivalently cd examples/hello && make rock.)
Plugins → Applications → hello. Flashes "Hello from Swift!" for one second (Swift calling into Rockbox), then "Swift says: 42" for three seconds (Rockbox displaying what Swift returned).
make EXAMPLE=cube rock
make EXAMPLE=cube install IPOD=/Volumes/<your-ipod>Plugins → Demos → cube. Controls (click wheel):
| Button | Action |
|---|---|
| LEFT / RIGHT | switch which axis (x/y/z) the wheel controls |
| scroll wheel | adjust that axis's spin speed (or angle, if paused) |
| MENU (hold) | cycle draw mode: solid / hidden-line / wireframe |
| PLAY | pause/resume rotation |
| SELECT (release) | toggle high-speed spin |
| MENU (release) | quit |
examples/cube/source/Cube.swift is the entire 3D pipeline -- rotation
matrix, perspective projection, backface culling, and (for SOLID) a
from-scratch scanline triangle rasterizer calling Rockbox.hline once
per row -- running every frame; examples/cube/plugin/cube.c is the
button/loop/draw-call host, ported from the original almost line-for-line.
Two differences from the original: plain Float + the shared shim's
sinf/cosf stand in for its Q14 fixed-point trig (the ARM940T has no
hardware float either way, so fixed-point bought the original nothing
dtype-safety wouldn't also give here), and SOLID's fill doesn't use
Rockbox's lib/mylcd+lib/grey plugin helper libraries the original's
does -- pulling those into the build wasn't worth it for what this repo
is demonstrating, and a from-scratch rasterizer is a better showcase of
Swift calling back into Rockbox per-scanline than a library call would
be anyway.
mkdir -p examples/<name>/{plugin,source}- Write
source/<Name>.swiftwith at least one@_cdeclentry point.import CRockboxto callrb_splash/sinf/cosf, or theRockbox.*wrapper for Rockbox-specific calls (seecommon/source/Rockbox.swift). - Write
plugin/<name>.c(plugin_start, calling into your Swift),plugin/SOURCES(list<name>.candrockbox_shim.c), andplugin/<name>.make(copyexamples/hello/plugin/hello.make, substituting the name). - Write
examples/<name>/Makefile(copyexamples/hello/Makefile, settingEXAMPLE/MODULE_NAME/CATEGORY/EXAMPLE_SWIFT_SRC). make EXAMPLE=<name> rock.
No UI-simulator target -- every example here is intentionally device-only.
A simulator build needs a host-architecture stand-in for the Swift object
(Embedded Swift can't link armv4t code into a host x86_64/arm64 binary);
see celeste-swift's ports/iPod, which uses a small C stub for exactly
that, for the pattern if an example here needs it.