Pangram verdict · v3.3
We believe that this text is a mix of AI and human-written content.
AI likelihood · overall
MixedArticle text · 1,412 words · 7 segments analyzed
Update (28th of September 2026) Max Desiatov told me that the @c swift attribute should work, instead of declaring the functions using @_cdecl. The @c attribute marks a global function as a C function implemented in Swift, which formalizes the @_cdecl attribute. A link to the proposal is available here. I did update the blog post and the code accordingly to that comment. Thanks Max! I started a small experiment: writing a very basic kernel in Swift, and running on QEMU. The goal is obviously not to replace Linux or any other popular kernel, but just to have fun and understand what is required to make a program run without an operating system underneath it. Actually I made a similar experiment years before with arOS 10 years ago. For the moment, the kernel does only one thing: it prints a message through QEMU and then waits forever, which is enough for a first deep dive. Swift Embedded # The project uses Embedded Swift, which is a subset of Swift designed for environments where there is no operating system and no standard library available (in the usual sense). My first Package.swift was very small: // swift-tools-version: 6.4 import PackageDescription let package = Package( name: "swift-kernel", platforms: [.macOS(.v14)], targets: [ .executableTarget( name: "swift_kernel", swiftSettings: [ .enableExperimentalFeature("Embedded"), .strictMemorySafety(), .treatAllWarnings(as: .error), ], ) ] ) And the first Swift file was almost insulting in its simplicity: // Sources/swift_kernel/test.swift print("Hello... world?") I tried to run it: > swift run <unknown>:0: error: unable to load standard library for target 'arm64-apple-macosx27.0.0' Oops. It seems like my current Swift compiler does not yet ship with the Embedded Swift standard library. As I missed in the documentation: Since Embedded Swift is still experimental and not yet supported in public Swift releases, you’ll need to use a development toolchain. So, let’s install the development toolchain and test again: > swiftly install main-snapshot && swiftly use main-snapshot > swift run [...] Hello... world? Success! At this point I had confirmed that Embedded Swift could compile and run a small program. The next step was to compile for a machine with no operating system at all. QEMU setup # For this project I will test on a machine emulator called QEMU, which is one of the most famous machine emulators for hackers. I am using QEMU’s virt machine on an Apple Silicon machine. The target is therefore aarch64-none-none-elf. This target, designed as a triple, describes a 64-bit ARM machine with no operating system and no environment provided by a C runtime.
When QEMU starts, the CPU is in a raw state. Before it can safely execute Swift code, I need to configure a stack and decide where the program starts. I will need a minimal assembly file that: defines the _start symbol, sets up a stack for the first core, jumps to our Swift entry function, kernel_main, and waits forever to keep the main running. Compiler setup # I have to tell the compiler that this is not a normal executable: the compiler must avoid linking the usual standard libraries, and the linker must use our linker script instead of a platform-specific one.
The project uses the following toolset.json, which will be passed to our Swift Package Manager (SPM): { "schemaVersion": "1.0", "swiftCompiler": { "extraCLIOptions": [ "-enable-experimental-feature", "Embedded", "-enable-experimental-feature", "Volatile", "-Xfrontend", "-no-allocations", "-Xfrontend", "-function-sections", "-Xfrontend", "-disable-stack-protector", "-Xlinker-driver", "-nostdlib", "-Xlinker-driver", "-fuse-ld=lld" ] }, "linker": { "extraCLIOptions": [ "-nostdlib", "-static", "--gc-sections", "--orphan-handling=error", "-T", "linker.ld", "-Map", ".build/kernel.map" ] } } Now, let’s build the project with the new target, the toolset, and Swift’s native build system: > swift build --triple aarch64-none-none-elf --toolset toolset.json --build-system native Unfortunately, the first linker attempt fails: ld.lld: error: section type mismatch for .strtab >>> <internal>:(.strtab): SHT_STRTAB >>> output section .nonalloc: SHT_SYMTAB ld.lld: error: section type mismatch for .shstrtab >>> <internal>:(.shstrtab): SHT_STRTAB >>> output section .nonalloc: SHT_PROGBITS ld.lld: error: undefined symbol: putchar ld.lld: error: undefined symbol: memmove Hmm. What is going on here? Because I am compiling for a bare-metal target (reminder: none-none-elf), there is no underlying operating system and no C standard library. Therefore there is no putchar, and there is no memmove either… The section errors have a different origin. With orphan handling enabled, the linker does not want to guess where ELF metadata should go. Therefore, the linker script must explicitly place the sections that I am keeping. Bridging Swift to assembly # I can’t use a normal Swift file with a main function. Instead, I expose a designated kernel entry function with @c, so that the assembly file can call it by its C-compatible name. @c func kernel_main() { print("Hello, Embedded Swift 😊") while true {} // Keep the program running } The assembly entry point lives in Sources/boot/boot.S: .section .text.boot .global _start _start: /* Read the CPU ID. We only want Core 0 to run our Swift code.
*/ mrs x1, mpidr_el1 and x1, x1, #3 cbz x1, 2f /* Park secondary cores in an infinite wait loop. */ 1: wfe b 1b /* Set up the stack for Core 0. */ 2: ldr x0, =__stack_top mov sp, x0 /* Jump to our Swift entry point. */ bl kernel_main /* Halt if we ever return. */ b 1b QEMU can start multiple virtual cores but, for now, only the first one should execute our kernel. The other cores wait (using wfe), waiting for an event that it will not send yet. Providing the missing functions # The print implementation used by Embedded Swift needs a way to write at least one character. On QEMU’s virt machine, the ARMs PL011 UART is mapped at 0x09000000. So putchar can write directly to that memory-mapped register: // QEMU's PL011 UART register let QEMU_UART_REGISTER: UInt = 0x0900_0000 @c @unsafe func putchar(_ c: CInt) -> CInt { let uart = unsafe UnsafeMutablePointer<UInt8>(bitPattern: QEMU_UART_REGISTER)! unsafe uart.pointee = UInt8(c) return c } This is not a screen driver but a serial output driver. QEMU will then display the bytes received by the emulated UART in the terminal (because I will execute it with the -nographic option). The other missing function is memmove. Swift’s low-level code needs basic memory operations, even when I am not using a normal standard library. For this first experiment, I provide a small implementation that copies forwards or backwards, depending on whether the source and destination overlap: @c @unsafe func memmove( _ dest: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ n: Int ) -> UnsafeMutableRawPointer { let d = unsafe dest.assumingMemoryBound(to: UInt8.self) let s = unsafe src.assumingMemoryBound(to: UInt8.self) if unsafe d < s { for i in 0..<n { unsafe d[i] = s[i] } } else if unsafe d > s { for i in (0..<n).reversed() { unsafe d[i] = s[i] } } return unsafe dest } This is the first point where the project stops being ordinary application programming. Here, I am no longer calling an operating-system API to print a string, but I am implementing the lower-level functions that the language runtime needs in order to exist. Describing the memory layout # Now that I did implement the Swift code, the linker needs to know where the kernel is loaded and where each part of the executable belongs. This is the role of linker.ld. QEMU’s virt machine loads an AArch64 kernel at 0x40080000 in this setup: ENTRY(_start) .
= 0x40080000; The linker script then places the boot code first, followed by read-only data, initialized data, uninitialized data, and a stack: .text : { KEEP(*(.text.boot)) *(.text*) } .rodata : ALIGN(8) { *(.rodata*) } .data : ALIGN(8) { *(.data*) } .bss (NOLOAD) : ALIGN(16) { __bss_start = .; *(.bss*) *(COMMON) __bss_end = .; } .stack (NOLOAD) : ALIGN(16) { . += 0x20000; __stack_top = .; } The assembly code uses the __stack_top symbol to initialize the stack pointer.
The stack is currently 128 kB, which is an unreasonable amount for a kernel that only prints one sentence, but it gives Swift some room to call functions while this project is still experimental. Finally, the script explicitly places the ELF metadata sections.
This avoids the orphan-section errors from the first linker attempt: /DISCARD/ : { *(.comment) *(.debug*) *(.eh_frame*) *(.swift_*) } .symtab : { *(.symtab) } .strtab : { *(.strtab) } .shstrtab : { *(.shstrtab) } Hello, Embedded Swift # Now, let’s build it > swift build --triple aarch64-none-none-elf --toolset toolset.json --build-system native , launched with QEMU: > qemu-system-aarch64 -machine virt -cpu cortex-a57 -nographic -kernel .build/aarch64-none-none-elf/debug/kernel and… Hello, Embedded Swift 😊 Then it keeps running forever, because the kernel is trapped in its infinite loop.