SwiftOS Developer Guide
This guide explains how to build user programs for SwiftOS. It covers the recommended native Embedded Swift path, the C/newlib compatibility path, and the workflow for adding programs to the base image.
For exact syscall and structure details, see API_REFERENCE.md. For platform, runtime, package, filesystem, and porting compatibility decisions, see COMPATIBILITY_GUIDE.md. For a practical source-port workflow, see PORTING_GUIDE.md. For copy-paste application recipes, see APPLICATION_COOKBOOK.md. For validation strategy, see TESTING_GUIDE.md. For package artifacts, signed repository fixtures, static-host publishing, and the current seed port fixtures, see PACKAGE_GUIDE.md and PACKAGE_BUILD_AUTOMATION.md.
Development Model
SwiftOS userland is static and rebuilt from source:
- Target architecture:
aarch64-none-none-elf. - User mode: EL0.
- Linking: static only.
- Dynamic loader: none.
- Primary language: Embedded Swift.
- C compatibility: newlib plus project-owned compatibility shims.
- Kernel ABI: SwiftOS POSIX-like syscall ABI, not Linux.
There are two supported application paths:
| Path | Use it for | Link inputs |
|---|---|---|
| Native Embedded Swift | First-party tools and modern SwiftOS apps | crt0.o, swift_user.o, your Swift object |
| C/newlib compat | Porting C programs and compatibility tools | crt0_newlib.o, newlib_syscalls.o, compat stubs, newlib, libm, libgcc |
Prefer native Embedded Swift for new SwiftOS programs.
Choose A Development Path
Start by deciding whether you are writing new SwiftOS software, adapting a C program, or packaging optional software. Then choose the delivery path and proof before adding build rules.
| Goal | Implementation Path | Delivery Path | Start From | Verification |
|---|---|---|---|---|
| Add a first-party command | Native Embedded Swift bridge | Base image /bin |
userland/echo.swift, userland/cat.swift, or userland/ls.swift |
Focused command test plus make build base-image |
| Add a network service | Native Embedded Swift or C/newlib with capNet |
Base image until service packaging is needed | userland/httpd.swift, userland/tcpecho.swift, or userland/udpecho.swift |
Matching network/service test from Service Guide |
| Port a small C utility | C/newlib compatibility path | Base image for core tools, package for optional tools | userland/newlibtest.c and Porting Guide |
In-QEMU command test and any host unit test for pure logic |
| Ship optional software | Source port recipe plus .swpkg package |
Package overlay, package store, signed repository, or seed repository | Application Cookbook package recipe and Package Guide | Matching package workflow test |
| Extend public app-facing API | Header, syscall bridge, or compatibility shim | Base image with updated headers/tools | API Reference and source-of-truth headers | API verification map plus focused QEMU behavior test |
Choose the delivery path before wiring the build:
| Delivery path | Use it when | Proof |
|---|---|---|
Base image /bin |
The command is part of the default OS image | make build base-image plus a QEMU command test |
Local .swpkg install |
The command is optional but should install inside the guest from a package file | make package-local-install-test |
| Signed repository fixture | The command should be resolved by package name through /bin/pkg |
make package-repo-install-test |
| Source port recipe | The command is maintained as an upstream source port | make ports-recipe-test; make ports-lua-repo-fixture; make ports-zlib-repo-fixture; make ports-bzip2-repo-fixture; make ports-ca-certificates-repo-fixture; make ports-pcre2-repo-fixture; make ports-tzdata-repo-fixture; make ports-nginx-repo-fixture; make ports-sqlite-repo-fixture; make ports-seed-repo-fixture; make package-ports-seed-repo-install-test; make ports-static-host-publish; make package-static-host-repo-install-test; make package-static-host-dns-repo-install-test |
Packages install under /usr today. Keep boot-critical tools in the immutable
base image, and use package workflows for optional commands and maintainer-side
porting proof.
Native Swift User Programs
Native tools import the C bridge declared in userland/lib/swift_user.h. The
Makefile passes it with:
-import-objc-header userland/lib/swift_user.h
The program entry point is a C-callable main:
@_cdecl("main")
func main(_ argc: Int32,
_ argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
_ envp: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?) -> Int32 {
_ = argc
_ = argv
_ = envp
swiftos_puts("hello from SwiftOS\n")
return 0
}
Rules of thumb:
- Do not import Foundation.
- Avoid host-only Swift APIs.
- Use
swiftos_*bridge calls for I/O, filesystem, network, process, time, mmap, thread, and futex operations. - Use
withUnsafeTemporaryAllocationfor short-lived buffers. - Keep long-lived heap use deliberate. The Swift runtime allocation hooks route
through the K&R-style allocator in
swift_user.c. - If dynamic
Stringcomparison or hashing needs Unicode data tables, link$(SWIFT_UNICODE_DATA)ascalcandkvdo.
Print Text
swiftos_puts("status: ok\n")
swiftos_putc(0x0A)
Write bytes to a file descriptor:
let message: StaticString = "raw bytes\n"
message.withUTF8Buffer {
_ = swiftos_write(1, $0.baseAddress, UInt($0.count))
}
Read A File
@_cdecl("main")
func main(_ argc: Int32,
_ argv: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?,
_ envp: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>?) -> Int32 {
_ = argc
_ = argv
_ = envp
let fd = swiftos_open("/etc/motd", 0)
if fd < 0 {
swiftos_puts("open failed\n")
return 1
}
withUnsafeTemporaryAllocation(byteCount: 128, alignment: 1) { buf in
let n = swiftos_read(fd, buf.baseAddress, UInt(buf.count))
if n > 0 {
_ = swiftos_write(1, buf.baseAddress, UInt(n))
}
}
_ = swiftos_close(fd)
return 0
}
Create A tmpfs File
The base filesystem is read-only. Write scratch data under /tmp:
let fd = swiftos_open("/tmp/example.txt", 0x40 | 1) // O_CREAT | O_WRONLY
if fd >= 0 {
let s: StaticString = "hello tmpfs\n"
s.withUTF8Buffer {
_ = swiftos_write(fd, $0.baseAddress, UInt($0.count))
}
_ = swiftos_close(fd)
}
The process needs capTmpWrite.
Use UDP
let fd = swiftos_socket()
if fd < 0 {
swiftos_puts("socket failed\n")
return 1
}
if swiftos_bind(fd, 5555) != 0 {
swiftos_puts("bind failed\n")
return 1
}
var ip: UInt32 = 0
var port: UInt16 = 0
withUnsafeTemporaryAllocation(byteCount: 512, alignment: 16) { buf in
let n = swiftos_recvfrom(fd, buf.baseAddress, UInt(buf.count), &ip, &port)
if n > 0 {
_ = swiftos_sendto(fd, buf.baseAddress, UInt(n), ip, port)
}
}
_ = swiftos_close(fd)
The process needs capNet.
Use TCP
let fd = swiftos_socket_stream()
if fd < 0 { return 1 }
if swiftos_bind(fd, 8080) != 0 { return 1 }
if swiftos_listen(fd, 8) != 0 { return 1 }
let client = swiftos_accept(fd)
if client >= 0 {
swiftos_puts("accepted\n")
_ = swiftos_close(client)
}
_ = swiftos_close(fd)
For a poll-driven server, follow userland/httpd.swift.
Use mmap With W^X
let len: UInt = 4096
let protRW = Int32(SWIFTOS_PROT_READ) | Int32(SWIFTOS_PROT_WRITE)
let protRX = Int32(SWIFTOS_PROT_READ) | Int32(SWIFTOS_PROT_EXEC)
let rw = swiftos_mmap(len, protRW)
if rw == 0 {
swiftos_puts("mmap failed\n")
return 1
}
// Write code or data into rw...
if swiftos_mprotect(rw, len, protRX) != 0 {
swiftos_puts("mprotect failed\n")
return 1
}
_ = swiftos_munmap(rw, len)
PROT_WRITE | PROT_EXEC is rejected.
Use Threads And Futexes
swiftos_thread_create(entry, arg, stack_top) starts another EL0 thread in the
same address space. The entry function must call swiftos_thread_exit() rather
than returning, because the thread enters from eret without a normal return
address.
Use the atomic helpers in swift_user.h plus swiftos_futex() to build locks.
See userland/threadsdemo.swift for the complete pattern.
Adding A Native Swift Program
Assume a new source file:
userland/mytool.swift
Add a userland artifact variable near the other USER_*_ELF variables:
USER_MYTOOL_ELF := $(BUILD)/mytool.elf
Add an object rule:
$(BUILD)/user_mytool.o: userland/mytool.swift userland/lib/swift_user.h Makefile | $(BUILD)/.dir
$(SWIFTC) $(USER_SWIFT_FLAGS) -c userland/mytool.swift -o $@
Add a link rule:
$(USER_MYTOOL_ELF): $(BUILD)/user_crt0.o $(BUILD)/user_swift_user.o $(BUILD)/user_mytool.o userland/user.ld Makefile
$(LDBIN) $(USER_LDFLAGS) $(BUILD)/user_crt0.o $(BUILD)/user_swift_user.o $(BUILD)/user_mytool.o -o $@
If the program needs Unicode data tables for dynamic String comparison or
hashing, add $(SWIFT_UNICODE_DATA) before -o $@.
Add the ELF to the base-image dependencies and copy it into $(BASE_ROOT)/bin
in the base-image recipe:
cp $(USER_MYTOOL_ELF) $(BASE_ROOT)/bin/mytool
Then build and boot:
make build base-image
make run
Inside the guest:
/bin/mytool
C Programs On The Raw Syscall ABI
Small C programs can use userland/lib/syscall.h directly:
#include "lib/syscall.h"
int main(void) {
const char msg[] = "hello from C\n";
write(1, msg, sizeof(msg) - 1);
return 0;
}
The raw wrapper style returns SwiftOS values directly. Most syscalls return a
nonnegative success value or a negative errno-like value. The raw wrappers do not
always set C errno.
C Programs Through newlib
Ported C software should use the newlib path:
Build the sysroot:
make newlibBuild or port the program with
aarch64-elf-gcc.Link statically with:
userland/lib/crt0_newlib.Suserland/lib/newlib_syscalls.cuserland/compat/stubs.csysroot/aarch64-elf/lib/libc.alibmandlibgccwhen needed
The compatibility layer provides source-level POSIX shapes for busybox, nginx probes, and similar software. It is not Linux ABI compatibility. It translates selected functions onto the SwiftOS syscall surface and stubs unsupported calls with conventional errors when possible.
Porting Checklist
When evaluating a C or Swift runtime port, check:
- Does it require dynamic linking? SwiftOS currently supports static linking only.
- Does it require Linux syscall numbers? SwiftOS does not provide them.
- Does it assume a persistent writable root filesystem? Use
/tmpor package image design instead. - Is it optional software rather than a default OS command? Prefer a
.swpkgpackage or ports recipe and prove the install path with package tests. - Does it require
forksemantics?forkexists for compatibility, but the preferred process model is explicit spawn with selected handles. - Does it require mmap executable pages? The supported pattern is RW mapping,
write code, then
mprotectto RX. - Does it need network sockets? The process must have
capNet. - Does it need filesystem reads or writes? The process must have
capFsReadand/orcapTmpWrite. - Does it need many file descriptors? Current compat
OPEN_MAXis small. - Does it require signals, process groups, or terminal ioctls beyond the current
shell subset? Check
userland/compat/stubs.c.
Testing A Program
Add at least one test that proves the behavior from outside the kernel:
- Host-side Swift unit test for pure logic.
- In-QEMU serial test for user-visible behavior.
- Network test with QEMU slirp and host tools such as
ncorcurl. - Boot acceptance marker if the program is part of a milestone.
Tests should be wired into make test when they are part of the standard
acceptance suite.
Common Mistakes
Importing Foundation
Do not import Foundation in native userland. The userland target is freestanding Embedded Swift.
Writing To The Base Image
The base filesystem is read-only. Use /tmp.
Returning From A Thread Entry
An EL0 thread entry must call swiftos_thread_exit().
Passing A Handle Without Rights
spawn_handles attenuates rights. The child cannot perform an operation unless
the inherited handle includes the needed right.
Assuming POSIX errno On Raw Wrappers
The raw syscall wrappers return negative errno-like values. The newlib compat
layer converts many calls to errno, but the raw bridge does not.
Source Examples
Good starting points:
| File | Shows |
|---|---|
userland/echo.swift |
Minimal argument handling and output |
userland/cat.swift |
File read loop |
userland/ls.swift |
stat, getdents, and formatting |
userland/httpd.swift |
Poll-driven TCP server and static file serving |
userland/udpecho.swift |
UDP sockets, IPv4, and IPv6 message layouts |
userland/threadsdemo.swift |
EL0 threads, atomics, futex |
userland/mmapdemo.swift |
mmap, munmap, mprotect, W^X |
userland/spawndemo.c |
spawn and spawn_handles |
userland/c4b_sockxfer.c |
Endpoint handle transfer |
userland/drvsvcdemo.c, userland/drvinputd.c |
Restartable service shape, device discovery, and opaque grant transfer |
Package and port examples:
| Path | Shows |
|---|---|
fixtures/pkghello/manifest.json |
Minimal .swpkg manifest |
ports/lang/lua/Port.json, ports/archivers/zlib/Port.json, ports/security/ca-certificates/Port.json |
Current source-port and data-package recipe shapes |
scripts/build-lua.sh, scripts/build-zlib.sh, scripts/build-ca-certificates.sh |
Static AArch64 source builds and data-package staging for signed repository fixtures |
tests/pkg_ports_seed_repo_install_test.sh |
Guest pkg install lua, pkg install zlib, pkg install ca-certificates, Lua smoke, minigzip, and CA bundle marker checks |