I've always wanted to really understand how an operating system works underneath — not the textbook block diagram, but the actual machinery: how a core comes up from reset, how address spaces get isolated, how a binary becomes a running process. I decided the most honest way to learn it was to build one. And I wanted to find out how far Embedded Swift could go if you took it all the way down to the metal.
The result is SwiftOS: a small but real operating system written almost entirely in Embedded Swift for 64-bit ARM. It boots on QEMU virt, and — the part I'm still a little amazed by — on a real Hetzner Cloud ARM VM, where it's currently serving its own website.
Site (served by SwiftOS itself): https://swiftos.tech
Source: GitHub - asaptf/swift-os: OS written in Embedded Swift · GitHub
The Embedded Swift side (the part I think this forum will care about)
- Toolchain: the swift.org 6.3.2-RELEASE toolchain, which ships an embedded stdlib for the
aarch64-none-none-elftarget — exactly what I build against. - Build flags, roughly:
-target aarch64-none-none-elf -enable-experimental-feature Embedded -wmo -parse-as-library -Osize -Xllvm -mattr=+strict-align,-neon -Xfrontend -function-sections -import-objc-header kernel/arch/aarch64/io.h - Freestanding: no Foundation, no full stdlib. The kernel is value types +
Unsafe*pointers at the lowest level,~Copyablestructs withdeinitfor resource ownership, and classes used sparingly and only after the heap is up (ARC isn't free). - Volatile MMIO goes through a tiny C bridge imported via
-import-objc-header(io.h); boot and exception vectors are assembly. Almost everything above that is Swift. - A few things that bit me and might save someone else time:
- You need
ld.lld, not GNUld— Embedded Swift's protected emptyArray/Stringsingletons land in a section GNU ld mishandles (and it clears a spurious RWX-segment warning too). String/Unicode pulls inlibswiftUnicodeDataTables.afrom the toolchain — you have to link it explicitly.print()andStringoutput lower toputchar, so the first thing the userland needs is a one-lineputcharshim over the UART/syscall.- Embedded heap allocations want 16-byte alignment — worth knowing when you're writing the first allocator over
sbrk.
- You need
What the OS actually does
- Real MMU isolation — one address space per process, capability-based handles instead of ambient authority.
- A native userland written in Swift — its own coreutils (
ls,ps,top, a calculator/REPL, …),console-login, andsshd, all on our ownsvcsyscall ABI. - An in-kernel TCP/IP stack — DHCP, TCP, UDP, DNS, HTTP, and TLS.
- A three-tier filesystem — an immutable, signed, read-only base image; a RAM
tmpfsscratch tier; and a persistent writable/datatier with honestfsync/fdatasync(durable enough to back SQLite). - SMP — it schedules across multiple cores (tested at
-smp 4), with cross-CPU TLB shootdown and spinlock-protected kernel state. - No Linux ABI, static linking only, no dynamic loader. Our own POSIX-like syscall surface; everything gets recompiled. There's a newlib port so "real" C/C++ software can link.
Getting real software to run
- nginx 1.30.2, statically linked against newlib + OpenSSL, serving HTTPS. This is what's serving the site above.
- Node.js 24.16.0 running on top of V8 in
--v8-lite-mode(jitless — which neatly sidesteps W^X on a kernel that doesn't hand out RWX pages).node --versionandnode -e "console.log(6*7)"→42both work. It was a long road: a fullaarch64-elfGCC 16.1 toolchain withlibstdc++built from source to satisfy V8's C++ runtime. (npm packaging is the next step.)
Running on real hardware
QEMU is forgiving; real hardware is not. Bringing SwiftOS up on a bare Hetzner Cloud ARM VM meant going from device tree + virtio-mmio + GICv2 to ACPI firmware tables (RSDP→MADT/MCFG/SPCR/GTDT, no DT fallback), GICv3, PCIe ECAM enumeration + virtio-pci, and DHCP over virtio-net-pci — then booting headless straight to sshd. That surfaced four bugs that only show up on real hardware (my favorite: PCIe ECAM and the 64-bit virtio window have to be mirrored into every process's page tables, or the kernel can touch the NIC but a userland process like sshd faults the moment it does TX/RX). All of it is gated by a regression test that boots the exact Hetzner topology.
Caveats, because this audience will (rightly) ask
It's minimal and there are rough edges. It started single-core; SMP is recent. Some things stay in C/asm on purpose — third-party code (busybox, newlib), the MMIO/boot/syscall bridges, and a couple of measured toolchain-limitation cases — but the design rule is Swift by default, C only with a documented reason.
I learned an enormous amount doing this — about ARM, virtual memory, drivers, the network stack, and how much "obvious" behavior we take for granted in mature kernels. I'd love feedback from people who actually know this domain: anything I've clearly got wrong, things you'd design differently, or Embedded Swift corners I should be using better. And if there's interest, I'm happy to write up specific parts in more depth — the allocator, the capability model, the SMP bring-up, or the V8-on-a-hobby-kernel saga.