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, callsclose(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- Returnstrueif the descriptor is >= 0explicit 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:
| Color | Code | Usage |
|---|---|---|
| Reset | \033[0m | Reset to default |
| Bold | \033[1m | Bold text |
| Dim | \033[2m | Dimmed text |
| Red | \033[31m | Errors |
| Green | \033[32m | Info |
| Yellow | \033[33m | Warnings |
| Blue | \033[34m | System |
| Magenta | \033[35m | Plugins |
| Cyan | \033[36m | Core |
| White | \033[37m | General |
| Bright variants | \033[9Xm | Alternative 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:
| Component | Color |
|---|---|
| waxed | Bright White |
| Paths | Cyan |
| PluginManager | Bright Magenta |
| PluginLoader | Magenta |
| PluginRegistry | Bright Cyan |
| CoreAPI | Bright Blue |
| IPCServer | Yellow |
| PropertyRegistry | Bright Yellow |
| RenderLoop | Bright Green |
| DRMDisplay | Green |
| AtomicKMS | Bright Red |
| SeatManager | Blue |
| Desktop | Bright Cyan |
| LoginScreen | Bright 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:
| Level | Color |
|---|---|
| TRACE | Dim |
| DEBUG | White |
| INFO | Green |
| WARN | Yellow |
| ERROR | Bright Red |
| CRITICAL | Bold 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:
| Macro | Level |
|---|---|
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:
| Macro | Level |
|---|---|
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
| Range | Category |
|---|---|
| 0 | Success |
| 1-10 | CLI Errors |
| 10-30 | seatd Errors |
| 30-50 | Plugin Loading Errors |
| 50-70 | Vulkan Errors |
| 70-90 | System Errors |
| 90-110 | DRM/KMS Errors |
| 110-120 | Crash Recovery Errors |
| 120-130 | Render Loop Errors |
| 130-140 | Atomic KMS Errors |
| 200+ | Test Errors |
| 250-270 | Property/IPC Errors |
| 270-280 | Vulkan Provider Errors |
| 280-300 | Cursor Service Errors |