Skip to content
Waxed Display Server
← Back to Docs

Utilities

Utilities

This document describes the core utility classes and functions used throughout Waxed.

1. UniqueFd - RAII File Descriptor Wrapper

UniqueFd is a move-only RAII wrapper for Unix file descriptors. It automatically closes the file descriptor when destroyed, preventing resource leaks.

Location: include/waxed/core/utils/unique_fd.h

Namespace: waxed::core::utils

1.1 Class Definition

namespace waxed::core::utils {

class UniqueFd {
public:
    UniqueFd() noexcept;
    explicit UniqueFd(int fd) noexcept;

    ~UniqueFd();

    // Move semantics
    UniqueFd(UniqueFd&& other) noexcept;
    UniqueFd& operator=(UniqueFd&& other) noexcept;

    // Copy is deleted
    UniqueFd(const UniqueFd&) = delete;
    UniqueFd& operator=(const UniqueFd&) = delete;

    // Operations
    [[nodiscard]] UniqueFd dup() const;
    void reset(int new_fd = -1) noexcept;
    [[nodiscard]] int release() noexcept;

    // Accessors
    [[nodiscard]] int get() const noexcept;
    [[nodiscard]] bool is_valid() const noexcept;
    explicit operator bool() const noexcept;
};

} // namespace waxed::core::utils

1.2 Constructors and Destructor

  • UniqueFd() noexcept - Default constructor, creates an empty (invalid) file descriptor (-1)
  • explicit UniqueFd(int fd) noexcept - Constructs from a raw file descriptor, takes ownership
  • ~UniqueFd() - Destructor, calls close(fd_) if the descriptor is valid

1.3 Move Semantics (No Copy)

UniqueFd is move-only. Copy operations are deleted to ensure single ownership of the file descriptor.

UniqueFd fd1(open("/dev/null", O_RDWR));
UniqueFd fd2 = std::move(fd1);  // OK: fd1 is now invalid, fd2 owns the FD
// fd1 is now -1, fd2 has the original FD

Move assignment properly handles self-assignment and closes the existing descriptor before taking ownership of the new one.

1.4 dup() Method

Creates a duplicate of the file descriptor using the Unix dup() system call. The duplicate is returned as a new UniqueFd instance.

[[nodiscard]] UniqueFd dup() const {
    if (fd_ < 0) return UniqueFd{};
    int new_fd = ::dup(fd_);
    return UniqueFd{new_fd};
}

If the current descriptor is invalid, returns an invalid UniqueFd.

1.5 reset() Method

Closes the current file descriptor (if valid) and optionally takes ownership of a new one.

void reset(int new_fd = -1) noexcept {
    if (fd_ >= 0) {
        ::close(fd_);
    }
    fd_ = new_fd;
}

Usage:

UniqueFd fd{open("/dev/zero", O_RDWR)};
fd.reset();  // Closes the FD, fd is now invalid
fd.reset(open("/dev/null", O_RDWR));  // Close old, take ownership of new

1.6 release() Method

Releases ownership of the file descriptor without closing it. Returns the raw file descriptor and sets the internal value to -1.

[[nodiscard]] int release() noexcept {
    return std::exchange(fd_, -1);
}

Use case: When you need to pass ownership to code that doesn’t use UniqueFd.

1.7 get() and is_valid()

  • int get() const noexcept - Returns the raw file descriptor value (may be -1 if invalid)
  • bool is_valid() const noexcept - Returns true if the descriptor is >= 0
  • explicit operator bool() const noexcept - Enables truthiness checks: if (fd) { ... }

1.8 Usage Patterns

Basic Usage

#include <waxed/core/utils/unique_fd.h>
#include <fcntl.h>

using waxed::core::utils::UniqueFd;

// Open a file and take ownership
UniqueFd fd{open("/path/to/file", O_RDONLY)};

if (!fd) {
    // Handle error
}

// Use with functions expecting raw file descriptors
write(fd.get(), "data", 4);

// Automatically closes when fd goes out of scope

With System Calls

UniqueFd socket_fd{socket(AF_UNIX, SOCK_STREAM, 0)};
if (socket_fd.is_valid()) {
    connect(socket_fd.get(), ...);
}
// socket_fd closes automatically here

Moving Between Scopes

auto create_fd() -> UniqueFd {
    UniqueFd fd{open("/dev/urandom", O_RDONLY)};
    return fd;  // Move construction
}

void consume_fd(UniqueFd fd) {
    // Takes ownership, will close when done
    read(fd.get(), buffer, size);
}

2. Logging System

Waxed uses a component-colored logging system built on top of spdlog. Each log entry includes a timestamp, log level, component name, and message-all color-coded for easy reading in the terminal.

Locations:

  • Header: include/waxed/logging.h
  • Implementation: src/core/logging.cpp

2.1 Log Output Format

Each log line has the following format:

[TIMESTAMP] LEVEL COMPONENT MESSAGE

Example:

[2025-02-23 14:32:01.456] info PluginManager Loading plugin from /usr/lib/waxed/libdesktop.so

2.2 ANSI Color Codes

The logging system uses ANSI escape sequences for colored terminal output:

ColorCodeUsage
Reset\033[0mReset to default
Bold\033[1mBold text
Dim\033[2mDimmed text
Red\033[31mErrors
Green\033[32mInfo
Yellow\033[33mWarnings
Blue\033[34mSystem
Magenta\033[35mPlugins
Cyan\033[36mCore
White\033[37mGeneral
Bright variants\033[9XmAlternative shades

Namespace: waxed::colors

namespace waxed::colors {
    constexpr const char* reset         = "\033[0m";
    constexpr const char* bold          = "\033[1m";
    constexpr const char* dim           = "\033[2m";
    constexpr const char* red           = "\033[31m";
    constexpr const char* green         = "\033[32m";
    constexpr const char* yellow        = "\033[33m";
    // ... etc
}

2.3 Component Colors

Each component gets a consistent color:

ComponentColor
waxedBright White
PathsCyan
PluginManagerBright Magenta
PluginLoaderMagenta
PluginRegistryBright Cyan
CoreAPIBright Blue
IPCServerYellow
PropertyRegistryBright Yellow
RenderLoopBright Green
DRMDisplayGreen
AtomicKMSBright Red
SeatManagerBlue
DesktopBright Cyan
LoginScreenBright Magenta

Unknown components receive a hash-based color from a fallback palette.

Function:

const char* get_component_color(const std::string& component);

2.4 Level Colors

Log levels are also color-coded:

LevelColor
TRACEDim
DEBUGWhite
INFOGreen
WARNYellow
ERRORBright Red
CRITICALBold Red

Function:

const char* get_level_color(spdlog::level::level_enum level);

2.5 DECLARE_LOGGER Macro

Place this macro at the top of your .cpp file to declare a default logging component:

#define DECLARE_LOGGER(comp) \
    static constexpr const char* LOG_COMPONENT = comp

Usage:

// At top of plugin_manager.cpp
DECLARE_LOGGER("PluginManager");

void some_function() {
    LOGC_INFO("Loading plugins...");  // Uses "PluginManager" automatically
}

2.6 LOGC_* Macros (Implicit Component)

These macros use the component declared with DECLARE_LOGGER:

MacroLevel
LOGC_TRACE(...)Trace
LOGC_DEBUG(...)Debug
LOGC_INFO(...)Info
LOGC_WARN(...)Warning
LOGC_ERROR(...)Error
LOGC_CRITICAL(...)Critical

Usage:

DECLARE_LOGGER("MyComponent");

void process() {
    LOGC_INFO("Starting process with count: {}", 42);
    LOGC_WARN("This is a warning");
    LOGC_ERROR("Error code: {}", errno);
}

2.7 LOGX_* Macros (Explicit Component)

These macros take the component name as the first argument. Use these when you cannot use DECLARE_LOGGER:

MacroLevel
LOGX_TRACE(comp, ...)Trace
LOGX_DEBUG(comp, ...)Debug
LOGX_INFO(comp, ...)Info
LOGX_WARN(comp, ...)Warning
LOGX_ERROR(comp, ...)Error
LOGX_CRITICAL(comp, ...)Critical

Usage:

// In header-only code or without DECLARE_LOGGER
LOGX_INFO("MyComponent", "Value: {}", value);
LOGX_ERROR("ErrorHandler", "Failed to open: {}", path);

2.8 Timestamp Formatting

The timestamp has the format YYYY-MM-DD HH:MM:SS.mmm with millisecond precision.

Performance Optimization: The date/time string is cached and only updated once per second. Only the milliseconds are computed on each log call.

std::string get_timestamp();
// Returns: "2025-02-23 14:32:01.456"

2.9 spdlog Integration

The logging system is built on spdlog but provides a custom formatting layer. Initialize with:

waxed::initialize_logging();
// Sets default level to info

Custom log levels can be set via spdlog:

spdlog::set_level(spdlog::level::debug);

2.10 Example Output

[2025-02-23 14:32:01.456] info PluginManager Loading plugin from /usr/lib/waxed/libdesktop.so
[2025-02-23 14:32:01.567] debug PluginLoader Found plugin_init symbol
[2025-02-23 14:32:01.678] warn RenderLoop Missed frame deadline
[2025-02-23 14:32:01.789] error AtomicKMS Failed commit: Invalid argument
[2025-02-23 14:32:01.890] critical CoreAPI Fatal error, shutting down

3. Error Handling

Waxed uses C++26’s std::expected for error handling without exceptions. The Result<T> type alias provides a consistent way to return either a value or an error code.

Location: include/waxed/error.h

3.1 Result Type Alias

template<typename T>
using Result = std::expected<T, ErrorCode>;

Result<T> represents either:

  • Success: Contains the value T
  • Failure: Contains an ErrorCode

3.2 ErrorCode Enum

Error codes are organized by category:

enum class ErrorCode : int {
    // Success (0)
    Success = 0,

    // CLI Errors (1-10)
    InvalidArguments = 1,
    MissingPluginPath = 2,
    HelpRequested = 3,
    RestartRequested = 4,

    // seatd Errors (10-30)
    SeatConnectionFailed = 10,
    SeatOpenFailed = 11,
    SeatOpenDeviceFailed = 12,
    // ...

    // Plugin Loading Errors (30-50)
    PluginNotFound = 30,
    PluginLoadFailed = 31,
    PluginSymbolNotFound = 32,
    // ...

    // Vulkan Errors (50-70) - Only in plugins
    VulkanInitializationFailed = 50,
    VulkanDisplayNotFound = 51,
    // ...

    // System Errors (70-90)
    FileDescriptorInvalid = 70,
    MemoryAllocationFailed = 71,
    // ...

    // DRM/KMS Errors (90-110)
    DRMFramebufferFailed = 90,
    DRMPageFlipFailed = 91,
    // ...

    // Atomic KMS Errors (130-140)
    DRMAtomicAllocFailed = 130,
    DRMAtomicCommitFailed = 131,
    // ...

    // Test Errors (200+)
    TestCrashSimulation = 200,

    // Property/IPC Errors (250-270)
    PropertyNotFound = 250,
    // ...

    // Vulkan Provider Errors (270-280)
    VulkanProviderAlreadyRegistered = 270,
    // ...

    // Cursor Service Errors (280-300)
    CursorNotInitialized = 280,
    // ...
};

3.3 to_string() Function

Converts an ErrorCode to a human-readable string:

auto to_string(ErrorCode code) -> std::string_view;

3.4 Using Result

Returning Success

auto open_device(const std::string& path) -> Result<int> {
    int fd = open(path.c_str(), O_RDWR);
    if (fd < 0) {
        return std::unexpected(ErrorCode::FileDescriptorInvalid);
    }
    return fd;
}

Returning Errors

auto load_plugin(const std::string& path) -> Result<void> {
    if (!std::filesystem::exists(path)) {
        return std::unexpected(ErrorCode::PluginNotFound);
    }
    // ...
    return {};
}

Checking Results

auto result = open_device("/dev/dri/card0");

if (!result) {
    // Handle error
    auto error = result.error();
    LOGC_ERROR("Failed to open device: {}", error);
    return error;
}

// Use the value
int fd = result.value();

Using value_or()

int fd = open_device("/dev/dri/card0").value_or(-1);

Chaining with and_then()

auto fd = open_device("/dev/dri/card0")
    .and_then([](int fd) {
        return configure_drm(fd);
    })
    .and_then([](int fd) {
        return create_framebuffer(fd);
    });

Monadic operations with transform()

auto size = get_framebuffer()
    .transform([](const Framebuffer& fb) {
        return fb.width * fb.height;
    });

3.5 fmt Integration

ErrorCode has a fmt formatter specialization, so it works directly with format strings:

LOGC_ERROR("Operation failed with code: {}", ErrorCode::PluginLoadFailed);
// Output: Operation failed with code: PluginLoadFailed

3.6 Error Categories by Range

RangeCategory
0Success
1-10CLI Errors
10-30seatd Errors
30-50Plugin Loading Errors
50-70Vulkan Errors
70-90System Errors
90-110DRM/KMS Errors
110-120Crash Recovery Errors
120-130Render Loop Errors
130-140Atomic KMS Errors
200+Test Errors
250-270Property/IPC Errors
270-280Vulkan Provider Errors
280-300Cursor Service Errors