conceptModern C++~5 min readUpdated 2026-07-02#cpp#serenityos#operating-systems#kernel#systems

SerenityOS: an operating system written in C++

SerenityOS matters because it is not a toy example in a blog post. It is a graphical Unix-like operating system, with kernel, userland, services, applications, libraries, ports, and tooling living in one public source tree. Its codebase shows the real bargain of C++ in OS work: you can encode ownership, errors, and invariants in types, but you still have to own the ABI, allocator, runtime, build system, and hardware boundary.

The reset: C++ does not make an OS less low-level. In a kernel, every abstraction still has to justify its object layout, failure mode, initialization order, and generated code.

How it really works

The SerenityOS repository describes the project as a graphical Unix-like OS for 64-bit x86, Arm, and RISC-V computers. The README also makes the architecture obvious: a kernel, POSIX-like userland, services, IPC, libraries, GUI applications, ports, build tools, and QEMU launch flow are all part of the system.

The interesting C++ lesson is not "use the standard library everywhere." SerenityOS has project libraries such as AK and custom types for ownership, formatting, containers, and errors. A kernel cannot blindly depend on hosted assumptions like process-wide exception policy, normal heap behavior, static initialization comfort, or the host platform's libc. Even in userland, the project has style rules that make code uniform: private members with m_, explicit naming conventions, range-for preference, C++ casts instead of C-style casts, and clear virtual override spelling.

The build reality is also part of the lesson. The current build instructions require a modern host compiler for host tools, CMake, Ninja, QEMU, and a cross-compilation flow from Linux, macOS, Windows with WSL2, and other Unix-like hosts. The default build path launches QEMU. That is exactly the kind of environment where your C++ subset is not just style; it is part of portability.

Executable artifact: a tiny Serenity-style systems subset

The runnable demo lives in examples/modern-cpp/serenityos-an-os-written-in-cpp/. It is not SerenityOS code. It is a small C++20 artifact that mirrors the shape of systems C++ decisions: RAII for a lock guard, an ErrorOr<T>-like return type, fixed-capacity storage, and a build with -fno-exceptions -fno-rtti.

cd examples/modern-cpp/serenityos-an-os-written-in-cpp
./run.sh

The guard is the same idea as every scoped lock in a kernel:

class SpinGuard {
public:
    explicit SpinGuard(SpinLock& lock)
        : m_lock(lock)
    {
        m_lock.lock();
    }

    ~SpinGuard()
    {
        m_lock.unlock();
    }

    SpinGuard(SpinGuard const&) = delete;
    SpinGuard& operator=(SpinGuard const&) = delete;

private:
    SpinLock& m_lock;
};

The fixed vector returns an explicit error instead of throwing or growing behind your back:

ErrorOr<std::size_t> append(T value)
{
    if (m_size == Capacity)
        return Error::NoSpace;

    m_storage[m_size] = value;
    return m_size++;
}

The script emits demo.O2.s. The lock guard destructor becomes an ordinary cleanup point. The ErrorOr result becomes ordinary value state. This is the heart of the OS-C++ argument: use types to keep protocols local, then inspect the lowered code.

Why SerenityOS is a useful study target

  • It is full-stack. Kernel, libc-like layers, IPC, services, GUI, libraries, and applications force different C++ trade-offs in one tree.
  • It is cross-compiled. You see host tools, target tools, image building, and emulator launch as one workflow.
  • It uses custom vocabulary. Types such as NonnullOwnPtr and ErrorOr show that serious C++ systems often define project-specific ownership and error shapes.
  • It is style-constrained. The coding style is explicit enough to make large-scale C++ readable across contributors.
  • It has real OS boundaries. Syscalls, process isolation, memory mappings, filesystem code, and drivers are where abstraction has to meet ABI facts.
  • It is current code. The project changes, so treat it as a source tree to read, not as a frozen tutorial.

Failure modes & trade-offs

  • Assuming hosted C++ rules apply in the kernel. Exceptions, RTTI, allocation, threads, and libc services all depend on runtime support.
  • Inventing a worse standard library by accident. Custom containers and smart pointers are justified in kernels, but they must still be documented, tested, and boring.
  • Letting style become folklore. A large C++ OS needs written rules because contributors otherwise import incompatible idioms from application C++.
  • Ignoring boot and initialization order. Global constructors, static state, and early allocator use can be harmless in an app and painful before the kernel is fully alive.
  • Cross-platform build drift. The supported host set, required compiler versions, emulator flags, and toolchain scripts are part of the project surface.
  • Reading without building. OS code often hides important facts in generated headers, compiler flags, linker scripts, and image-building scripts.

In practice

Read SerenityOS like a map of decisions. Start with the README and build instructions, then inspect AK ownership/error types, kernel syscall boundaries, and a small userland service. Ask the same questions everywhere: who owns this object, who can fail, who allocates, who may block, what crosses a C ABI boundary, and what code runs during cleanup?

For your own OS project, the lesson is not to copy SerenityOS wholesale. The lesson is to make a C++ subset that matches your runtime. If your kernel has no exception runtime, use explicit errors. If your allocator is not always available, avoid hidden growth. If your ABI must be C-shaped at the syscall edge, wrap it deliberately. C++ can help, but only when the subset is honest about the machine.

Connects to: Which subset of C++ to use · Error handling: exceptions vs expected · RAII: the idea that changes everything · Smart pointers: unique_ptr, shared_ptr, ownership · Translation units, declarations, and linkage · The pipeline: preprocess, compile, assemble, link · The syscall: crossing into the kernel · Craftsmanship for low-level programmers

Sources

  • SerenityOS README - current project overview, supported architectures, features, and build/run entry points. https://github.com/SerenityOS/serenity
  • SerenityOS Build Instructions - current host dependencies, compiler requirements, cross-build flow, and QEMU expectations. https://github.com/SerenityOS/serenity/blob/master/Documentation/BuildInstructions.md
  • SerenityOS Coding Style - project-specific C++ naming, formatting, pointer/reference, cast, and virtual override conventions. https://github.com/SerenityOS/serenity/blob/master/Documentation/CodingStyle.md
  • SerenityOS NonnullOwnPtr source - concrete ownership vocabulary in the AK library. https://github.com/SerenityOS/serenity/blob/master/AK/NonnullOwnPtr.h
  • SerenityOS syscall API source - example of C++ types meeting kernel/userland syscall boundaries. https://github.com/SerenityOS/serenity/blob/master/Kernel/API/Syscall.h
  • SerenityOS top-level CMake - shows target system checks, toolchain expectations, image targets, and build integration. https://github.com/SerenityOS/serenity/blob/master/CMakeLists.txt
  • C++ Core Guidelines - useful contrast for hosted C++ guidance versus the narrower subset an OS kernel may choose. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines