ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
Abstraction Layers

πŸ”Œ Cross-platform abstraction for threading, sockets, and system APIs

πŸ”Œ Cross-platform abstraction for threading, sockets, and system APIs

Platform-independent APIs for Windows, Linux, and macOS.

Platform Abstraction Layer README

Overview

The platform abstraction layer provides a unified API for cross-platform development across Windows, Linux, and macOS. It eliminates platform-specific conditional compilation from application code by centralizing OS-specific implementations.

Cross-platform development without abstraction typically requires conditional compilation (#ifdef blocks) scattered throughout the codebase, with Windows APIs differing significantly from POSIX systems. This approach reduces code readability and increases maintenance complexity.

The platform abstraction layer solves this by providing a single, consistent API that works identically across all supported platforms. Application code calls platform functions with a unified interface; implementation details translate these calls to the appropriate platform-specific APIs at runtime.

What does the platform layer abstract?

  • Threading: Create threads, join them, get thread IDsβ€”same API everywhere
  • Synchronization: Mutexes, read-write locks, condition variables
  • Networking: Sockets that work the same on Windows and POSIX
  • Terminal I/O: Control the terminal, detect capabilities, raw mode
  • System functions: Environment variables, sleep, signals, crash handling
  • File I/O: Safe, portable file operations with proper error handling
  • String operations: Safe string handling across platforms

The Big Achievement: Zero platform-specific code (#ifdef blocks) in application code!

Supported Platforms: Windows 10+, Linux (POSIX), macOS (POSIX)

Design Philosophy

The platform abstraction layer follows a comprehensive philosophy that guides every design decision. Understanding these principles will help you use the platform layer effectively and understand why it works the way it does.

Complete Isolation

Principle: Platform-specific code is 100% isolated in implementation files.

This is the most important principleβ€”application code (src/) has ZERO #ifdef blocks for platform detection. All platform differences live in lib/platform/posix/ or lib/platform/windows/.

Why this matters:

// ❌ BAD - Platform checks scattered everywhere
#ifdef _WIN32
HANDLE thread;
CreateThread(...);
#else
pthread_t thread;
pthread_create(...);
#endif
// βœ… GOOD - Single API, works everywhere
asciichat_thread_create(&thread, thread_func, arg);
#define asciichat_thread_create(thread_ptr, attr, start_routine, arg)
void * asciichat_thread_t

Benefits of complete isolation:

  • Clean separation: Platform bugs are fixed in one location
  • Better readability: Application code focuses on business logic
  • Easier testing: Can mock platform layer for unit tests
  • Maintainability: Changes to platform code don't affect application

Zero Overhead in Release Builds

Principle: Abstraction should add zero runtime cost in production.

The platform layer achieves this through:

  • Direct function calls (no function pointers or virtual dispatch)
  • Inline wrappers for simple operations
  • Compile-time selection of platform code

In release builds:

// This abstraction call:
mutex_lock(&my_mutex);
// Compiles directly to:
// Windows: EnterCriticalSection(&my_mutex.cs);
// POSIX: pthread_mutex_lock(&my_mutex.mutex);
#define mutex_lock(mutex)
Lock a mutex (with debug tracking in debug builds)

No runtime overhead, no performance penalty! The abstraction disappears at compile time.

Optional Debug Support

Principle: Enable extensive debugging without affecting production performance.

Debug builds can enable lock tracking, memory debugging, and verbose logging:

// Enable debug features at compile time
#define DEBUG_THREADS // Track thread creation/destruction
#define DEBUG_LOCKS // Track lock acquisition/release
#define DEBUG_MEMORY // Track allocations
// No runtime cost in release builds!

This gives you powerful debugging when you need it, zero cost when you don't.

POSIX-First API Design

Principle: Use POSIX semantics as the baseline, adapt Windows to match.

Why POSIX first?

  • POSIX is the standard for Unix-like systems (Linux, macOS, BSD)
  • Well-documented, stable APIs that have been around for decades
  • Most developers already know POSIX threading and socket APIs

The abstraction:

  1. Defines API based on POSIX semantics
  2. POSIX implementation is thin wrapper (often just delegation)
  3. Windows implementation adapts Windows APIs to POSIX semantics

Example:

// POSIX version (simple delegation)
return pthread_mutex_lock(&mutex->mutex);
}
// Windows version (adaptation)
EnterCriticalSection(&mutex->cs);
return 0; // Windows doesn't return error codes like POSIX
}
mutex_t mutex
Mutex type (POSIX: pthread_mutex_t with debug tracking)

Strong Type Safety

Principle: Use distinct types to prevent platform-specific mistakes.

Platform types are completely opaque to application code:

// Platform-specific types (application never sees these)
typedef struct {
#ifdef _WIN32
HANDLE handle;
#else
pthread_t thread;
#endif
// Application just uses asciichat_thread_t
asciichat_thread_create(&my_thread, worker, NULL);

This prevents common mistakes like:

  • Comparing Windows HANDLE to -1 (wrong, should be NULL)
  • Comparing POSIX socket to NULL (wrong, should be -1)
  • Using platform-specific functions on wrong types

Static Initialization

Principle: Support global synchronization primitives with zero runtime init cost.

You can declare global mutexes, locks, and condition variables without explicit initialization functions:

// Global mutex - works on all platforms, no init() call needed
void critical_function() {
static_mutex_lock(&g_mutex);
// Critical section
static_mutex_unlock(&g_mutex);
}
Static mutex structure for global mutexes requiring static initialization.

How it works:

  • POSIX: Uses compile-time initializers (PTHREAD_MUTEX_INITIALIZER)
  • Windows: Uses lazy initialization with atomic compare-exchange

Both approaches are thread-safe and require zero explicit initialization!

Consistent Semantics

Principle: The platform layer normalizes differences that don't matter to the application.

What gets normalized:

  • Type unification: Socket handles are socket_t everywhere
  • Error code normalization: Error codes use errno convention everywhere
  • Function signature normalization: Thread functions use void* (*)(void*)
  • Flag normalization: File operations use POSIX-style flags

Application code can assume consistent semantics without worrying about platform differences in function signatures, error handling, or data types.

Minimal Abstraction Surface

Principle: Expose only what's needed.

The platform layer doesn't try to abstract everything. Instead, it provides a focused API that covers only what ascii-chat needs:

  • Threading (thread creation, joining, detaching)
  • Synchronization (mutexes, rwlocks, condition variables)
  • Networking (sockets with auto-optimization)
  • Terminal I/O (raw mode, cursor control, size detection)
  • System utilities (environment, strings, error handling)
  • Debugging support (backtraces, symbol resolution)

Complex platform features that ascii-chat doesn't use (e.g., Windows COM, macOS CoreFoundation, Linux-specific ioctls) are left out entirely. This keeps the abstraction layer small, maintainable, and focused.

Benefits of minimalism:

  • Faster compilation: Fewer headers, smaller API surface
  • Easier maintenance: Less code to maintain, fewer edge cases
  • Better focus: API designed for ascii-chat's specific needs

Architecture

The platform abstraction layer uses a three-layer architecture that keeps everything clean and organized.

Three-Layer Design

Layer 1: Header Definition (lib/platform/*.h)

  • Defines the public API that application code uses
  • Declares abstract types (asciichat_thread_t, mutex_t, socket_t, etc.)
  • Documents all functions with Doxygen comments
  • Completely platform-agnostic

Layer 2: POSIX Implementation (lib/platform/posix/*.c)

  • Implements the API using POSIX standards
  • Thin wrappers around pthread, BSD sockets, termios
  • Works on Linux, macOS, BSD, and other Unix-like systems

Layer 3: Windows Implementation (lib/platform/windows/*.c)

  • Implements the same API using Windows APIs
  • Adapts Windows semantics to match POSIX behavior
  • Uses Windows threads, Critical Sections, Winsock2, Console API

The build system automatically selects Layer 2 or Layer 3 based on the target platform.

Directory Structure

lib/platform/
β”œβ”€β”€ README.md # Platform abstraction documentation
β”œβ”€β”€ abstraction.h # Main header - includes everything
β”œβ”€β”€ abstraction.c # Common implementation (minimal)
β”œβ”€β”€ init.h # Static initialization helpers
β”œβ”€β”€ util.h # Public utilities (ssize_t, aligned memory, errors, files)
β”œβ”€β”€ internal.h # Private helpers (ONLY for lib/platform/* files)
β”‚
β”œβ”€β”€ thread.h # Thread API definition
β”œβ”€β”€ mutex.h # Mutex API definition
β”œβ”€β”€ rwlock.h # Read-write lock API definition
β”œβ”€β”€ cond.h # Condition variable API definition
β”œβ”€β”€ socket.h # Socket API definition
β”œβ”€β”€ terminal.h # Terminal I/O API definition
β”œβ”€β”€ system.h # System functions API definition
β”œβ”€β”€ string.h # String operations API definition
β”œβ”€β”€ file.h # File I/O API definition
β”œβ”€β”€ password.h # Password input API definition
β”‚
β”œβ”€β”€ posix/ # POSIX implementations (Linux/macOS)
β”‚ β”œβ”€β”€ thread.c
β”‚ β”œβ”€β”€ mutex.c
β”‚ β”œβ”€β”€ rwlock.c
β”‚ β”œβ”€β”€ cond.c
β”‚ β”œβ”€β”€ socket.c
β”‚ β”œβ”€β”€ terminal.c
β”‚ β”œβ”€β”€ system.c
β”‚ └── symbols.c # Backtrace using execinfo.h
β”‚
└── windows/ # Windows implementations
β”œβ”€β”€ thread.c # Most complex (2500+ lines!)
β”œβ”€β”€ mutex.c
β”œβ”€β”€ rwlock.c
β”œβ”€β”€ cond.c
β”œβ”€β”€ socket.c
β”œβ”€β”€ terminal.c
β”œβ”€β”€ system.c
β”œβ”€β”€ symbols.c # Backtrace using StackWalk64
β”œβ”€β”€ getopt.c # POSIX getopt for Windows
β”œβ”€β”€ windows_compat.h # Windows.h wrapper
└── windows_errno.h # POSIX errno on Windows

Header Organization

Most application code only needs to include one header:

#include "platform/abstraction.h" // Includes everything you need
Platform abstraction layer umbrella header providing unified cross-platform API.

For specific components, you can include individual headers:

#include "platform/thread.h" // Just threading
#include "platform/socket.h" // Just sockets
#include "platform/terminal.h" // Just terminal I/O
🧡 Cross-platform thread interface for ascii-chat
Cross-platform socket interface for ascii-chat.
πŸ–₯️ Cross-platform terminal interface for ascii-chat

The abstraction.h header automatically includes all component headers, so you get the complete platform abstraction API in one include.

Note
Important: platform/internal.h is a private header that MUST only be included within the lib/platform/ directory. External code should never include it directly. All public utilities are available through platform/util.h, platform/abstraction.h, or specific component headers like platform/socket.h.

Core Components

The platform abstraction is organized into focused components, each handling a specific aspect of cross-platform development.

Threading (thread.h)

Thread creation, joining, and management with timeout support.

Basic thread operations:

void* worker_thread(void* arg) {
int* value = (int*)arg;
printf("Worker processing: %d\n", *value);
return NULL;
}
int main() {
// Create thread
int data = 42;
int result = asciichat_thread_create(&thread, worker_thread, &data);
if (result != 0) {
log_error("Thread creation failed");
return 1;
}
// Wait for thread to finish
asciichat_thread_join(&thread, NULL);
platform_cleanup();
return 0;
}
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
asciichat_error_t platform_init(void)
Initialize platform-specific subsystems.
#define asciichat_thread_join(thread, timeout_ms)
int main(int argc, char *argv[])
Definition main.c:472

Thread operations with timeout:

// Wait up to 5 seconds for thread to complete
int result = asciichat_thread_join_timeout(&thread, NULL, 5000);
if (result == ETIMEDOUT) {
log_warn("Thread did not finish in time");
// Thread is still running!
} else if (result == 0) {
log_info("Thread completed successfully");
}
#define log_warn(...)
Log a WARN message.
Definition log/log.h:574
#define log_info(...)
Log an INFO message.
Definition log/log.h:561
int asciichat_thread_join_timeout(asciichat_thread_t *thread, void **retval, uint64_t timeout_ns)
Wait for a thread to complete with timeout.
#define ETIMEDOUT

Thread identity and comparison:

// Get current thread ID
// Compare thread IDs
if (asciichat_thread_equal(my_id, other_id)) {
log_debug("Same thread");
}
// Get numeric thread ID for logging
log_debug("Thread ID: %" PRIu64, tid);
unsigned long long uint64_t
Definition common.h:59
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
uint64_t asciichat_thread_current_id(void)
Get the current thread's unique numeric ID.
Definition threading.c:92
#define asciichat_thread_self()
#define asciichat_thread_equal(t1, t2)

Important platform differences:

  • Windows: Timeout join fully supported
  • macOS: Timeout join falls back to blocking (no pthread_timedjoin_np)
  • Linux: Full timeout join support via pthread_timedjoin_np

Mutexes (mutex.h)

Mutual exclusion locks for protecting shared data.

Basic mutex usage:

typedef struct {
mutex_t lock;
int counter;
} shared_data_t;
void increment_counter(shared_data_t* data) {
mutex_lock(&data->lock);
data->counter++;
mutex_unlock(&data->lock);
}
int main() {
shared_data_t data;
mutex_init(&data.lock);
data.counter = 0;
// Use the data with multiple threads...
mutex_destroy(&data.lock);
return 0;
}
int mutex_init(mutex_t *mutex, const char *name)
Initialize a mutex with a name.
Definition threading.c:16
#define mutex_unlock(mutex)
Unlock a mutex (with debug tracking in debug builds)
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
Definition threading.c:22

Try-lock (non-blocking):

int result = mutex_trylock(&data->lock);
if (result == 0) {
// Lock acquired!
data->counter++;
mutex_unlock(&data->lock);
} else {
// Lock was busy, couldn't acquire it
log_debug("Lock busy, skipping operation");
}
#define mutex_trylock(mutex)
Try to lock a mutex without blocking (with debug tracking in debug builds)

Static initialization for global mutexes:

// Global mutex - no explicit init needed!
void update_config(const char* key, const char* value) {
static_mutex_lock(&g_config_lock);
// Update configuration
static_mutex_unlock(&g_config_lock);
}

Platform implementations:

  • POSIX: pthread_mutex_t with error checking enabled
  • Windows: CRITICAL_SECTION with 4000 spin count for performance

Read-Write Locks (rwlock.h)

Read-write locks allow multiple concurrent readers but only one writer.

Why use read-write locks?

  • Performance: Multiple threads can read simultaneously
  • Correctness: Writers get exclusive access for modifications
  • Scalability: Ideal for read-heavy workloads

Basic usage:

typedef struct {
rwlock_t lock;
char* data;
} shared_resource_t;
// Multiple readers can access simultaneously
const char* read_data(shared_resource_t* resource) {
rwlock_rdlock(&resource->lock);
const char* result = resource->data; // Read operation
rwlock_rdunlock(&resource->lock);
return result;
}
// Writers get exclusive access
void write_data(shared_resource_t* resource, const char* new_data) {
rwlock_wrlock(&resource->lock);
free(resource->data);
resource->data = strdup(new_data); // Write operation
rwlock_wrunlock(&resource->lock);
}
#define rwlock_wrunlock(lock)
Release a write lock (with debug tracking in debug builds)
Definition rwlock.h:349
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:294
#define rwlock_wrlock(lock)
Acquire a write lock (with debug tracking in debug builds)
Definition rwlock.h:313
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:331
Read-write lock type (POSIX: pthread_rwlock_t with debug tracking)
Definition rwlock.h:61

Real-world example from ascii-chat:

// Global client list protected by read-write lock
static client_t** g_clients = NULL;
static size_t g_client_count = 0;
// Broadcasting - many threads read concurrently
void broadcast_frame(const frame_t* frame) {
static_rwlock_rdlock(&g_client_manager_rwlock);
for (size_t i = 0; i < g_client_count; i++) {
send_frame_to_client(g_clients[i], frame);
}
static_rwlock_unlock(&g_client_manager_rwlock);
}
// Adding client - requires exclusive write access
void add_client(client_t* client) {
static_rwlock_wrlock(&g_client_manager_rwlock);
g_clients = realloc(g_clients, (g_client_count + 1) * sizeof(client_t*));
g_clients[g_client_count++] = client;
static_rwlock_unlock(&g_client_manager_rwlock);
}
int frame
Definition splash.c:99
client_info_t * add_client(server_context_t *server_ctx, socket_t socket, const char *client_ip, int port)
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
Frame structure that stores both data and actual size.
Definition ringbuffer.h:383
Static reader-writer lock structure for global rwlocks requiring static initialization.

Platform implementations:

  • POSIX: pthread_rwlock_t with standard semantics
  • Windows: SRWLOCK (Slim Reader-Writer Lock) - only 8 bytes!

Important note on Windows:

  • Windows SRWLocks don't distinguish between read/write unlock
  • Both rwlock_rdunlock() and rwlock_wrunlock() call ReleaseSRWLock*()
  • Use the explicit unlock functions for code clarity anyway

Condition Variables (cond.h)

Condition variables enable threads to wait for specific conditions to become true.

Basic pattern:

typedef struct {
mutex_t lock;
cond_t cond;
bool ready;
} sync_t;
// Thread 1: Wait for condition
void wait_for_ready(sync_t* sync) {
mutex_lock(&sync->lock);
while (!sync->ready) {
cond_wait(&sync->cond, &sync->lock); // Atomically unlocks and waits
}
// Condition is now true, lock is held
mutex_unlock(&sync->lock);
}
// Thread 2: Signal condition
void set_ready(sync_t* sync) {
mutex_lock(&sync->lock);
sync->ready = true;
cond_signal(&sync->cond); // Wake up one waiter
mutex_unlock(&sync->lock);
}
#define cond_wait(cond, mutex)
Wait on a condition variable (with debug tracking in debug builds)
Definition cond.h:275
int cond_signal(cond_t *cond)
Signal a condition variable (wake one waiting thread)
Condition variable type (POSIX: pthread_cond_t with debug tracking)
Definition cond.h:63

Timeout waiting:

mutex_lock(&sync->lock);
while (!sync->ready) {
int result = cond_timedwait(&sync->cond, &sync->lock, 5000); // 5 seconds
if (result == ETIMEDOUT) {
log_warn("Timeout waiting for condition");
break;
}
}
mutex_unlock(&sync->lock);
#define cond_timedwait(cond, mutex, timeout_ns)
Wait on a condition variable with timeout (with debug tracking in debug builds)
Definition cond.h:300

Broadcasting to multiple waiters:

// Wake up ALL waiting threads
mutex_lock(&sync->lock);
sync->ready = true;
cond_broadcast(&sync->cond); // Wake everyone
mutex_unlock(&sync->lock);
int cond_broadcast(cond_t *cond)
Broadcast to a condition variable (wake all waiting threads)

Static initialization:

// Global condition variable
static bool g_shutdown = false;
void wait_for_shutdown() {
static_mutex_lock(&g_shutdown_lock);
while (!g_shutdown) {
static_cond_wait(&g_shutdown_cond, &g_shutdown_lock);
}
static_mutex_unlock(&g_shutdown_lock);
}
void trigger_shutdown() {
static_mutex_lock(&g_shutdown_lock);
g_shutdown = true;
static_cond_broadcast(&g_shutdown_cond);
static_mutex_unlock(&g_shutdown_lock);
}
static_cond_t g_shutdown_cond
Global shutdown condition variable for waking blocked threads.
Static condition variable structure for global condition variables requiring static initialization.

Sockets (socket.h)

Network socket operations with automatic optimization and error handling.

Basic server:

int main() {
platform_init(); // Initializes Winsock on Windows
// Create socket
socket_t server_sock = socket_create(AF_INET, SOCK_STREAM, 0);
if (!socket_is_valid(server_sock)) {
log_error("Socket creation failed: %s", socket_get_error_string());
return 1;
}
// Set socket options
socket_set_reuseaddr(server_sock, true);
socket_set_nodelay(server_sock, true);
// Bind and listen
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(27224);
if (socket_bind(server_sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
log_error("Bind failed: %s", socket_get_error_string());
socket_close(server_sock);
return 1;
}
socket_listen(server_sock, 10);
// Accept connections
socket_t client_sock = socket_accept(server_sock, NULL, NULL);
if (socket_is_valid(client_sock)) {
// Handle client...
socket_close(client_sock);
}
socket_close(server_sock);
platform_cleanup(); // Cleanup Winsock on Windows
return 0;
}
int socket_set_nodelay(socket_t sock, bool nodelay)
Set TCP_NODELAY socket option (disable Nagle's algorithm)
bool socket_is_valid(socket_t sock)
Check if a socket handle is valid.
int socket_close(socket_t sock)
Close a socket.
int socket_bind(socket_t sock, const struct sockaddr *addr, socklen_t addrlen)
Bind a socket to an address.
const char * socket_get_error_string(void)
Get last socket error as string.
socket_t socket_accept(socket_t sock, struct sockaddr *addr, socklen_t *addrlen, const char *name)
Accept an incoming connection.
int socket_set_reuseaddr(socket_t sock, bool reuse)
Set SO_REUSEADDR socket option.
socket_t socket_create(const char *name, int domain, int type, int protocol)
Create a new named socket.
int socket_listen(socket_t sock, int backlog)
Listen for incoming connections.
int socket_t

Automatic socket optimization:

When you accept a connection, the platform layer automatically optimizes it:

socket_t client_sock = socket_accept(server_sock, NULL, NULL);
// Automatically applied:
// - TCP_NODELAY enabled (low latency)
// - 2MB send/recv buffers (falls back to 512KB, then 128KB)
// - 5s send timeout, 10s recv timeout
// - SO_KEEPALIVE enabled

You can override these settings if needed:

socket_set_nodelay(client_sock, false); // Re-enable Nagle
socket_setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, ...); // Custom timeout
int socket_setsockopt(socket_t sock, int level, int optname, const void *optval, socklen_t optlen)
Set socket option.

Non-blocking I/O:

// Make socket non-blocking
socket_set_nonblocking(client_sock, true);
// Now send/recv return immediately with EWOULDBLOCK if not ready
ssize_t sent = socket_send(client_sock, data, size, 0);
if (sent < 0) {
// Would block, try again later
}
}
int socket_set_nonblocking(socket_t sock, bool nonblocking)
Set socket to non-blocking mode.
#define EWOULDBLOCK
ssize_t socket_send(socket_t sock, const void *buf, size_t len, int flags)
Send data on a socket.
int socket_get_last_error(void)
Get last socket error code.

Polling multiple sockets:

struct pollfd fds[2];
fds[0].fd = socket_get_fd(server_sock);
fds[0].events = POLLIN;
fds[1].fd = socket_get_fd(client_sock);
fds[1].events = POLLIN | POLLOUT;
int ready = socket_poll(fds, 2, 1000); // 1 second timeout
if (ready > 0) {
if (fds[0].revents & POLLIN) {
// Server socket ready for accept
}
if (fds[1].revents & POLLIN) {
// Client socket ready for read
}
}
int socket_get_fd(socket_t sock)
Get the underlying file descriptor (POSIX compatibility)
int socket_poll(struct pollfd *fds, nfds_t nfds, int64_t timeout_ns)
Poll sockets for events (multiplexed I/O)

Platform differences:

  • Windows: Requires socket_init() to initialize Winsock (done by platform_init())
  • POSIX: No initialization needed
  • Error codes: Automatically normalized to POSIX errno values

Terminal I/O (terminal.h)

Terminal control, cursor manipulation, and capability detection.

Basic terminal setup:

int main() {
// Get terminal size
if (terminal_get_size(&size) == 0) {
printf("Terminal: %dx%d\n", size.cols, size.rows);
}
// Check capabilities
printf("Terminal supports color!\n");
}
printf("Terminal supports Unicode!\n");
}
platform_cleanup();
return 0;
}
bool terminal_supports_unicode(void)
Check if terminal supports unicode.
asciichat_error_t terminal_get_size(terminal_size_t *size)
Get terminal size.
bool terminal_supports_color(void)
Check if terminal supports color.
Terminal size structure.
Definition terminal.h:163
int cols
Number of columns (width) in terminal.
Definition terminal.h:165
int rows
Number of rows (height) in terminal.
Definition terminal.h:164

Raw mode for interactive applications:

// Enable raw mode (no line buffering, no echo)
// Now read individual keypresses
char c;
read(STDIN_FILENO, &c, 1);
printf("You pressed: %c\n", c);
// Restore normal mode
asciichat_error_t terminal_set_raw_mode(bool enable)
Set terminal to raw mode.
asciichat_error_t terminal_set_echo(bool enabled)
Set terminal echo mode.

Cursor control:

// Hide cursor
terminal_hide_cursor(true);
// Move cursor to specific position (1-based)
terminal_move_cursor(10, 20); // Row 10, Column 20
// Save/restore cursor position
printf("Hello from top-left!");
terminal_restore_cursor(); // Back to original position
// Show cursor again
terminal_hide_cursor(false);
asciichat_error_t terminal_save_cursor(void)
Save cursor position.
asciichat_error_t terminal_move_cursor(int row, int col)
Move cursor to specified position.
asciichat_error_t terminal_restore_cursor(void)
Restore saved cursor position.

Screen manipulation:

// Clear entire screen
// Set terminal title
terminal_set_title("ascii-chat v1.0");
// Ring the terminal bell
// Set scroll region (for double-buffering)
terminal_set_scroll_region(5, 20); // Rows 5-20 can scroll
// Reset terminal to default state
asciichat_error_t terminal_reset(int fd)
Reset terminal to default state.
Definition misc.c:81
asciichat_error_t terminal_set_title(const char *title)
Set terminal window title.
asciichat_error_t terminal_clear_screen(void)
Clear the terminal screen.
asciichat_error_t terminal_set_scroll_region(int top, int bottom)
Set scroll region.
asciichat_error_t terminal_ring_bell(void)
Ring terminal bell.

Platform-specific features:

  • Windows: Automatically enables ANSI escape sequences on Windows 10+
  • POSIX: Full termios support for all terminal control

System Functions (system.h)

System-level operations: sleep, signals, crash handlers, process info.

Sleep operations:

// Sleep for 1 second
// Short sleep for timing loops
platform_sleep_ms(16); // ~60 FPS
void platform_sleep_ms(unsigned int ms)
Sleep for a specified number of milliseconds.

Platform sleep precision:

  • Linux/macOS: Microsecond precision with nanosleep()
  • Windows: ~15ms minimum resolution (system timer granularity)

Process information:

// Get current process ID
int pid = platform_get_pid();
log_info("Process ID: %d", pid);
// Get current username
const char* user = platform_get_username();
log_info("Running as: %s", user);
pid_t platform_get_pid(void)
Get the current process ID.
Definition wasm/system.c:61
const char * platform_get_username(void)
Get the current username.

Environment variables:

// Get environment variable
const char* home = platform_getenv("HOME");
if (home) {
printf("Home directory: %s\n", home);
}
// Set environment variable
platform_setenv("MY_VAR", "my_value");
int platform_setenv(const char *name, const char *value)
Set an environment variable.
Definition wasm/system.c:50
const char * platform_getenv(const char *name)
Get an environment variable value.
Definition wasm/system.c:39

TTY detection:

// Check if stdout is a terminal
if (platform_isatty(STDOUT_FILENO)) {
printf("Running in a terminal\n");
const char* tty = platform_ttyname(STDOUT_FILENO);
printf("TTY device: %s\n", tty);
} else {
printf("Output is redirected\n");
}
const char * platform_ttyname(int fd)
Get the name of the terminal associated with a file descriptor.
int platform_isatty(int fd)
Check if a file descriptor is a terminal.
Definition util.c:63

Signal handling:

// Signal handler function
void handle_sigint(int sig) {
log_info("Caught SIGINT, shutting down...");
g_shutdown = true;
}
// Register signal handler (thread-safe on all platforms)
platform_signal(SIGINT, handle_sigint);
signal_handler_t platform_signal(int sig, signal_handler_t handler)
Set a signal handler.
Definition misc.c:115

Crash handlers (automatic backtrace on crash):

// Crash handlers are automatically installed by platform_init()!
int main() {
platform_init(); // Installs crash handlers
// If the program crashes, you get a backtrace:
// *** CRASH DETECTED ***
// Signal: SIGSEGV (Segmentation fault)
//
// === BACKTRACE ===
// 0: main
// 1: some_function
// 2: another_function
// ================
platform_cleanup();
return 0;
}

Manual backtrace (for debugging):

// Print backtrace right now
platform_print_backtrace(0); // 0 = don't skip any frames
// Get backtrace for custom processing
void* buffer[64];
int count = platform_backtrace(buffer, 64);
char** symbols = platform_backtrace_symbols(buffer, count);
for (int i = 0; i < count; i++) {
printf("%s\n", symbols[i]);
}
platform_backtrace_symbols_free(symbols);
int platform_backtrace(void **buffer, int size)
Get a backtrace of the current call stack.
Definition util.c:16
char ** platform_backtrace_symbols(void *const *buffer, int size)
Convert backtrace addresses to symbol names.
Definition util.c:22
void platform_print_backtrace(int skip_frames)
Definition util.c:44

Platform-specific signal support:

  • POSIX: Full signal support (SIGINT, SIGTERM, SIGWINCH, etc.)
  • Windows: Limited support (SIGINT, SIGTERM work but SIGWINCH is a no-op)

String Operations (string.h)

Safe string handling that works consistently across platforms.

Safe formatting:

// Safe snprintf (handles Windows _snprintf quirks)
char buffer[64];
platform_snprintf(buffer, sizeof(buffer), "Value: %d", 42);
// Always null-terminates, even on Windows

Safe string copy:

// BSD strlcpy semantics on all platforms
char dest[32];
size_t copied = platform_strlcpy(dest, "Hello, world!", sizeof(dest));
// dest is guaranteed to be null-terminated
// copied is the length of the source string
size_t platform_strlcpy(char *dst, const char *src, size_t size)
Safe string copy with size tracking (strlcpy)

Safe string concatenation:

char buffer[64] = "Hello";
platform_strlcat(buffer, ", world!", sizeof(buffer));
// buffer is guaranteed to be null-terminated
size_t platform_strlcat(char *dst, const char *src, size_t size)

Case-insensitive comparison:

if (platform_strcasecmp("hello", "HELLO") == 0) {
printf("Strings are equal (case-insensitive)\n");
}
if (platform_strncasecmp("hello", "HELLO", 3) == 0) {
printf("First 3 characters match\n");
}
int platform_strncasecmp(const char *s1, const char *s2, size_t n)
Case-insensitive string comparison with length limit.
int platform_strcasecmp(const char *s1, const char *s2)
Case-insensitive string comparison.

String duplication:

// Duplicate entire string
char* copy = platform_strdup("Hello, world!");
printf("%s\n", copy);
free(copy);
// Duplicate first N characters
char* partial = platform_strndup("Hello, world!", 5);
printf("%s\n", partial); // "Hello"
free(partial);
char * platform_strdup(const char *s)
Duplicate string (strdup replacement)

Thread-safe tokenization:

char input[] = "one,two,three";
char* saveptr;
char* token = platform_strtok_r(input, ",", &saveptr);
while (token) {
printf("Token: %s\n", token);
token = platform_strtok_r(NULL, ",", &saveptr);
}
char * platform_strtok_r(char *str, const char *delim, char **saveptr)
Thread-safe string tokenization (strtok_r replacement)

File I/O (file.h)

Platform-safe file operations.

Opening files:

// Open with platform-specific handling
int fd = platform_open("config.txt", O_RDWR | O_CREAT, 0600);
if (fd < 0) {
log_error("Failed to open file: %s", strerror(errno));
return -1;
}
int platform_open(const char *name, const char *pathname, int flags,...)
Safe file open (open replacement)
int errno

Reading and writing:

char buffer[1024];
ssize_t bytes_read = platform_read(fd, buffer, sizeof(buffer));
if (bytes_read < 0) {
log_error("Read failed: %s", strerror(errno));
}
const char* data = "Hello, file!";
ssize_t bytes_written = platform_write(fd, data, strlen(data));
if (bytes_written < 0) {
log_error("Write failed: %s", strerror(errno));
}
ssize_t platform_read(int fd, void *buf, size_t count)
Safe file read (read replacement)
ssize_t platform_write(int fd, const void *buf, size_t count)
Safe file write (write replacement)
Definition wasm/system.c:72
atomic_t bytes_written
Definition mmap.c:42

Closing files:

if (platform_close(fd) < 0) {
log_error("Close failed: %s", strerror(errno));
}
int platform_close(int fd)
Safe file close (close replacement)

Force sync to disk:

// Ensure data is written to disk
if (platform_fsync(fd) < 0) {
log_error("Fsync failed: %s", strerror(errno));
}
int platform_fsync(int fd)
Synchronize a file descriptor to disk.

Filesystem Operations (filesystem.h)

High-level cross-platform filesystem operations: directories, paths, and file discovery. This header provides unified directory management, path utilities, and configuration file discovery across Windows and POSIX systems.

Directory operations:

// Create a single directory (no-op if exists)
if (platform_mkdir("~/.ascii-chat", 0700) == ASCIICHAT_OK) {
printf("Directory created or already exists\n");
}
// Create directories recursively (like mkdir -p)
if (platform_mkdir_recursive("~/.ascii-chat/config/keys", 0700) == ASCIICHAT_OK) {
printf("Directory tree created\n");
}
// Clean up: recursively delete directory and contents
if (platform_rmdir_recursive("~/.ascii-chat/temp") == ASCIICHAT_OK) {
printf("Directory removed\n");
}
@ ASCIICHAT_OK
Definition error_codes.h:51
asciichat_error_t platform_mkdir(const char *path, int mode)
Create a directory.
asciichat_error_t platform_mkdir_recursive(const char *path, int mode)
Create directories recursively (mkdir -p equivalent)
asciichat_error_t platform_rmdir_recursive(const char *path)
Recursively delete a directory and all its contents.

File type checking and statistics:

// Check file type without following symlinks
if (platform_is_regular_file("config.toml")) {
printf("config.toml is a regular file\n");
}
if (platform_is_directory("./data")) {
printf("./data is a directory\n");
}
// Get detailed file metadata
platform_stat_t stat_info;
if (platform_stat("myfile", &stat_info) == ASCIICHAT_OK) {
printf("File size: %zu bytes\n", stat_info.size);
printf("Is symlink: %d\n", stat_info.is_symlink);
}
asciichat_error_t platform_stat(const char *path, platform_stat_t *stat_out)
Get file statistics.
int platform_is_directory(const char *path)
Check if a path is a directory.
int platform_is_regular_file(const char *path)
Check if a path is a regular file.
Definition util.c:124
File type information from stat()
Definition filesystem.h:232
int is_symlink
Non-zero if file is a symbolic link.
Definition filesystem.h:237
size_t size
File size in bytes.
Definition filesystem.h:233

Path utilities:

// Check if path is absolute
if (platform_path_is_absolute("/etc/config")) {
printf("Absolute path\n"); // Unix
}
if (platform_path_is_absolute("C:\\Users\\data")) {
printf("Absolute path\n"); // Windows
}
// Get platform-specific path separator
printf("Separator: %c\n", sep); // '/' on Unix, '\\' on Windows
// Normalize path (converts separators, validates)
char normalized[PATH_MAX];
platform_path_normalize("C:\\Users\\..\\data", normalized, sizeof(normalized));
printf("Normalized: %s\n", normalized);
char platform_path_get_separator(void)
Get the path separator character for current platform.
asciichat_error_t platform_path_normalize(const char *input, char *output, size_t output_size)
Normalize and validate a file path.
bool platform_path_is_absolute(const char *path)
Check if a path is absolute (not relative)

Temporary files and directories:

// Create temporary directory with unique suffix
char tempdir[256];
if (platform_mkdtemp(tempdir, sizeof(tempdir), "ascii-chat-gpg") == ASCIICHAT_OK) {
printf("Temp dir: %s\n", tempdir); // e.g., /tmp/ascii-chat-gpg-ABC123
// ... use tempdir for sensitive operations ...
platform_rmdir_recursive(tempdir); // Clean up
}
// Create temporary file with proper permissions
char tempfile[256];
int fd;
if (platform_create_temp_file(tempfile, sizeof(tempfile), "asc_sig", &fd) == 0) {
// On Unix: fd is open and ready for writing
// On Windows: fd == -1, must open with platform_temp_file_open()
if (fd < 0) {
platform_temp_file_open(tempfile, &fd);
}
write(fd, data, size);
close(fd);
}
int platform_create_temp_file(char *path_out, size_t path_size, const char *prefix, int *fd)
Create a temporary file with a given prefix.
asciichat_error_t platform_temp_file_open(const char *name, const char *path, int *fd_out)
Open a temporary file for writing.
asciichat_error_t platform_mkdtemp(char *path_out, size_t path_size, const char *prefix)
Create a temporary directory with a given prefix.
int platform_delete_temp_file(const char *path)
Delete a temporary file.

Configuration file discovery:

// Find config file across standard locations
// Returns all matches in priority order (highest priority first)
config_file_list_t config_list = {0};
if (platform_find_config_file("config.toml", &config_list) == ASCIICHAT_OK) {
// Use first match (highest priority) - Override semantics
if (config_list.count > 0) {
printf("Using config: %s\n", config_list.files[0].path);
load_config(config_list.files[0].path);
}
config_file_list_destroy(&config_list);
}
// Cascade semantics: load all configs (system to user)
if (platform_find_config_file("colors.toml", &config_list) == ASCIICHAT_OK) {
// Load in reverse order (lowest priority first)
// Higher priority configs override lower priority
for (size_t i = config_list.count; i > 0; i--) {
apply_config(config_list.files[i - 1].path, &theme);
}
config_file_list_destroy(&config_list);
}
asciichat_error_t platform_find_config_file(const char *filename, config_file_list_t *list_out)
Find config file across multiple standard locations.
void config_file_list_destroy(config_file_list_t *list)
Free config file list resources.
List of config file search results.
Definition filesystem.h:595
size_t count
Number of results found.
Definition filesystem.h:597
config_file_result_t * files
Array of results (allocated, must be freed)
Definition filesystem.h:596
char * path
Absolute path to config file (allocated, must be freed)
Definition filesystem.h:583

Search paths by platform:

  • Linux/macOS: ~/.config/ascii-chat/ β†’ ~/.local/share/ascii-chat/ β†’ /etc/ascii-chat/
  • Windows: APPDATA%\ascii-chat\ β†’ PROGRAMDATA%\ascii-chat\

Home and config directory discovery:

// Get home directory (platform-aware)
const char* home = platform_get_home_dir();
printf("Home: %s\n", home); // e.g., "/home/user" or "C:\\Users\\user"
// Get app config directory (with trailing separator)
char* config_dir = platform_get_config_dir();
printf("Config dir: %s\n", config_dir);
// Linux: /home/user/.config/ascii-chat/
// macOS: /Users/user/.config/ascii-chat/
// Windows: C:\Users\user\AppData\Roaming\ascii-chat\
SAFE_FREE(config_dir);
// Get app data directory
char* data_dir = platform_get_data_dir();
printf("Data dir: %s\n", data_dir);
// Linux: /home/user/.local/share/ascii-chat/
// macOS: /Users/user/.local/share/ascii-chat/
// Windows: C:\Users\user\AppData\Roaming\ascii-chat\
SAFE_FREE(data_dir);
char * platform_get_data_dir(void)
Get the application data directory.
const char * platform_get_home_dir(void)
Get the user's home directory.
Definition util.c:94
char * platform_get_config_dir(void)
Get the application configuration directory.
Definition util.c:104

Cryptographic key file validation:

// Validate key file permissions (critical for security!)
// Checks that only owner can read the private key
printf("Key file permissions are secure\n");
} else {
printf("Key file has insecure permissions! Use chmod 600\n");
}
asciichat_error_t platform_validate_key_file_permissions(const char *key_path)
Validate that a cryptographic key file has appropriate permissions.

Platform-specific details:

  • POSIX: Uses chmod, stat, mkdir, opendir for all operations
  • Windows: Uses CreateDirectoryA, GetFileAttributesEx, ACLs for permissions
  • Symlinks: All functions skip symlinks (use lstat/no-follow logic)
  • Path separators: Automatically handled (/ or \ based on platform)
  • Permissions: Unix mode parameter ignored on Windows (uses ACLs)
  • Relative paths: Expanded from current directory (not home)

Static Initialization

One of the most powerful features of the platform abstraction layer is static initialization. You can declare global synchronization primitives without any explicit initialization code!

Why Static Initialization?

Traditional approach (error-prone):

// ❌ BAD - Requires explicit initialization
static pthread_mutex_t g_mutex;
static bool g_mutex_initialized = false;
void init() {
if (!g_mutex_initialized) {
pthread_mutex_init(&g_mutex, NULL);
g_mutex_initialized = true;
}
}
void use_mutex() {
if (!g_mutex_initialized) {
// Race condition! What if another thread is in init()?
init();
}
pthread_mutex_lock(&g_mutex);
// ...
}

Platform abstraction approach (correct):

// βœ… GOOD - No initialization needed!
void use_mutex() {
static_mutex_lock(&g_mutex); // Just works!
// ...
static_mutex_unlock(&g_mutex);
}

How It Works

Initialization timing differs between platforms based on OS capabilities:

POSIX (Linux/macOS):

// Compile-time initialization
#define STATIC_MUTEX_INIT { .mutex = PTHREAD_MUTEX_INITIALIZER, .initialized = true }
// The mutex is fully initialized at compile time!

Windows:

// Runtime lazy initialization (but thread-safe!)
#define STATIC_MUTEX_INIT { .cs = {0}, .initialized = false }
int static_mutex_lock(static_mutex_t* mutex) {
if (!mutex->initialized) {
// Atomic compare-exchange ensures only one thread initializes
if (InterlockedCompareExchange(&mutex->initialized, 1, 0) == 0) {
InitializeCriticalSection(&mutex->cs);
mutex->initialized = 1;
} else {
// Another thread is initializing, wait for it
while (!mutex->initialized) {
Sleep(0); // Yield to other threads
}
}
}
EnterCriticalSection(&mutex->cs);
return 0;
}

Both approaches are:

  • Thread-safe: No race conditions
  • Zero-overhead: No runtime cost on POSIX, minimal cost on Windows
  • Transparent: Same API on all platforms

Static Initialization Usage

Global mutex:

#include "platform/init.h"
static config_t* g_config = NULL;
void set_config(const char* key, const char* value) {
static_mutex_lock(&g_config_mutex);
// Update config
static_mutex_unlock(&g_config_mutex);
}
Platform initialization and static synchronization helpers.

Global read-write lock:

static cache_t* g_cache = NULL;
const char* cache_get(const char* key) {
static_rwlock_rdlock(&g_cache_lock);
const char* value = cache_lookup(g_cache, key);
static_rwlock_unlock(&g_cache_lock);
return value;
}
void cache_set(const char* key, const char* value) {
static_rwlock_wrlock(&g_cache_lock);
cache_insert(g_cache, key, value);
static_rwlock_unlock(&g_cache_lock);
}

Global condition variable:

static bool g_work_available = false;
void worker_thread() {
while (true) {
static_mutex_lock(&g_work_mutex);
while (!g_work_available) {
static_cond_wait(&g_work_cond, &g_work_mutex);
}
// Do work
g_work_available = false;
static_mutex_unlock(&g_work_mutex);
}
}
void submit_work() {
static_mutex_lock(&g_work_mutex);
g_work_available = true;
static_cond_signal(&g_work_cond);
static_mutex_unlock(&g_work_mutex);
}

Threading API Reference

Complete reference for all threading, synchronization, and thread-local storage APIs.

Thread Lifecycle Management

Thread Creation:

void *(*func)(void *), void *arg);

Creates a new thread that executes func(arg). The thread handle is stored in *thread.

  • Parameters:
    • thread: Output parameter for thread handle (must be initialized with asciichat_thread_init or uninitialized)
    • func: Thread function with signature void* (*)(void*)
    • arg: Single void pointer argument passed to thread function
  • Return: 0 on success, non-zero on error
  • Thread safety: Safe to call from any thread
  • Platform notes:
    • Windows: Uses CreateThread()
    • POSIX: Uses pthread_create()

Error handling example:

int result = asciichat_thread_create(&thread, worker, data);
if (result != 0) {
log_error("Failed to create thread: %s", strerror(result));
return;
}
void asciichat_thread_init(asciichat_thread_t *thread)
Initialize a thread handle to an uninitialized state.

Thread Exit:

void asciichat_thread_exit(void *retval);
#define asciichat_thread_exit(status)

Terminates the calling thread and passes retval to any waiting joiners.

  • This function never returns
  • Called implicitly when thread function returns
  • Only necessary if you need to exit before return statement

Thread Joining (blocking wait):

int asciichat_thread_join(asciichat_thread_t *thread, void **retval);

Blocks until the specified thread terminates.

  • Parameters:
    • thread: Thread handle from asciichat_thread_create()
    • retval: Optional output parameter for thread return value (NULL to ignore)
  • Return: 0 on success, non-zero on error
  • Blocks: Calling thread waits indefinitely for target thread to finish
  • Usage: Always used in cleanup or synchronization points

Thread Joining with Timeout:

void **retval, uint64_t timeout_ns);

Wait for thread with maximum wait time.

  • Parameters:
    • timeout_ns: Timeout in nanoseconds (1,000,000 ns = 1 ms)
  • Return: 0 if thread finished, non-zero on timeout or error
  • Use case: Graceful shutdown with timeout, watchdog patterns
  • Platform notes:
    • macOS: Falls back to blocking join (no timeout support in POSIX)
    • Linux: Full support via pthread_timedjoin_np()
    • Windows: Full support

Timeout example (graceful shutdown):

void shutdown_worker(asciichat_thread_t *worker) {
// Signal thread to stop (set flag, send message, etc.)
tell_thread_to_stop(worker);
// Wait up to 5 seconds
uint64_t timeout = 5 * 1000000000ULL; // 5 seconds in nanoseconds
int result = asciichat_thread_join_timeout(worker, NULL, timeout);
if (result != 0) {
log_warn("Thread did not stop gracefully");
// On real systems: force-kill, cleanup resources, etc.
}
}

Thread Identification

Get Current Thread ID:

Returns the calling thread's ID.

  • Return type: thread_id_t (platform-specific, may be int, long, or struct)
  • Use case: Compare with other thread IDs, logging with thread context
  • Unique per thread: Each thread has a distinct ID throughout its lifetime

Compare Thread IDs:

Safely compare two thread IDs for equality.

Get Numeric Thread ID:

Returns a unique numeric identifier for the current thread.

  • Return: 64-bit unsigned integer unique per thread
  • Advantage: Always comparable with ==, safe for hashing
  • Use case: Hash tables, thread-local registries, concise logging

Thread Registry Example:

typedef struct {
char name[64];
int client_fd;
void register_thread(const char *name, int client_fd) {
.client_fd = client_fd,
};
platform_strlcpy(entry.name, name, sizeof(entry.name));
registry_add(&g_registry, &entry);
}
int thread_id
Definition debug/mutex.c:47
thread_id_t thread_id
Definition debug/mutex.c:48

Check Thread Initialization:

bool asciichat_thread_is_initialized(asciichat_thread_t *thread)
Check if a thread handle has been initialized.

Check/set thread handle initialization state.

  • Use case: Pool structures, static initialization verification
  • thread_init() resets handle to uninitialized state
  • Check before re-using handles from pools

Thread Pool Example:

typedef struct {
bool available;
thread_pool_entry_t* acquire_thread_from_pool() {
for (size_t i = 0; i < POOL_SIZE; i++) {
if (!asciichat_thread_is_initialized(&g_pool[i].thread)) {
// Thread handle is available, can be reused
return &g_pool[i];
}
}
return NULL; // No available threads
}
Thread pool entry (internal linked list node)
Definition thread_pool.h:99
struct thread_pool_entry thread_pool_entry_t
Definition thread_pool.h:80

Mutex Operations

Mutex Initialization:

int mutex_init(mutex_t *mutex, const char *name);

Initialize and destroy mutexes.

  • name: Human-readable name for debugging (e.g., "recv_lock", "state_mutex")
  • Must call mutex_init() before any lock/unlock operations
  • Must call mutex_destroy() when done (usually in cleanup)
  • Never destroy a locked mutex

Locking (blocking):

Acquires the mutex, blocking until available.

  • Blocks: Thread waits if another thread holds the lock
  • Return: 0 on success (lock acquired)
  • Recursive: NOT recursive - deadlocks if same thread locks twice
  • Must be paired: Always unlock when done

Try-Lock (non-blocking):

Attempts to acquire the mutex without blocking.

  • Return: 0 if lock acquired, non-zero if already locked
  • Use case: Non-blocking operations, avoiding deadlock patterns
  • Busy-wait pattern: Don't loop on trylock; use condition variables instead

Unlocking:

Releases the mutex, potentially waking waiting threads.

  • Precondition: Mutex must be locked by calling thread
  • Return: 0 on success
  • Error check: Always check return value (indicates misuse)

Critical Section Pattern:

struct shared_data {
mutex_t lock;
int counter;
char* buffer;
};
void increment_counter(struct shared_data *data) {
if (mutex_lock(&data->lock) != 0) {
log_error("Lock failed");
return;
}
// Critical section - protected from concurrent access
data->counter++;
if (mutex_unlock(&data->lock) != 0) {
log_error("Unlock failed");
}
}

Condition Variables

Condition Variable Initialization:

int cond_init(cond_t *cond, const char *name);
int cond_destroy(cond_t *cond);
int cond_init(cond_t *cond, const char *name)
Initialize a condition variable with a name.
int cond_destroy(cond_t *cond)
Destroy a condition variable.

Initialize and destroy condition variables.

  • Must initialize before use
  • Always pair with a mutex for synchronization
  • Destroy when no longer needed

Waiting:

Atomically unlocks mutex and waits for condition to be signaled.

  • Mutex state:
    • Must be locked BEFORE calling cond_wait()
    • Atomically unlocked while waiting
    • Re-locked before returning
  • Spurious wakeups: Check condition in a loop, don't assume it's true

Timed Waiting:

int cond_timedwait(cond_t *cond, mutex_t *mutex, uint64_t timeout_ns);

Wait with timeout.

  • Return: 0 if signaled, non-zero on timeout or error
  • Timeout unit: Nanoseconds (1,000,000,000 ns = 1 second)
  • Use case: Resource initialization with timeout, watchdog patterns

Signaling:

int cond_signal(cond_t *cond); // Wake one waiter
int cond_broadcast(cond_t *cond); // Wake all waiters

Wake waiting threads.

  • cond_signal(): Wakes one arbitrary waiter
  • cond_broadcast(): Wakes all waiters
  • Lost signals: If no threads are waiting, signal is lost
  • Best practice: Always check condition after waking

Producer-Consumer with Timeout:

typedef struct {
int* items;
int size;
int count;
int head, tail;
mutex_t lock;
cond_t not_empty;
cond_t not_full;
} queue_t;
int queue_pop_timeout(queue_t *q, int *item, uint64_t timeout_ns) {
mutex_lock(&q->lock);
// Wait for item with timeout
while (q->count == 0) {
int result = cond_timedwait(&q->not_empty, &q->lock, timeout_ns);
if (result != 0) {
// Timeout occurred
mutex_unlock(&q->lock);
return -1;
}
}
// Item available
*item = q->items[q->head];
q->head = (q->head + 1) % q->size;
q->count--;
cond_signal(&q->not_full);
mutex_unlock(&q->lock);
return 0;
}

Thread-Local Storage

TLS Key Management:

int ascii_tls_key_create(tls_key_t *key, void (*destructor)(void *));
int ascii_tls_key_create(tls_key_t *key, void(*destructor)(void *))
Create a thread-local storage key.
Definition threading.c:97
int ascii_tls_key_delete(tls_key_t key)
Delete a thread-local storage key.
Definition threading.c:101

Create and destroy thread-local storage keys.

  • destructor: Optional function called with stored value when thread exits (NULL if not needed)
  • One key is shared across all threads
  • Each thread stores a different value for that key

TLS Get/Set:

int ascii_tls_set(tls_key_t key, void *value);
int ascii_tls_set(tls_key_t key, void *value)
Set thread-local value for a key.
Definition threading.c:109
void * ascii_tls_get(tls_key_t key)
Get thread-local value for a key.
Definition threading.c:105

Get and set thread-local values.

  • tls_get(): Returns value for current thread, or NULL if not set
  • tls_set(): Sets value for current thread
  • Each thread has independent values

Thread-Local Registry Pattern:

// Global TLS key for client context
static tls_key_t g_client_context_key;
typedef struct {
int client_id;
char* name;
// ... other client data ...
} client_context_t;
void cleanup_client_context(void *value) {
client_context_t *ctx = (client_context_t *)value;
if (ctx) {
free(ctx->name);
free(ctx);
}
}
void initialize_tls() {
ascii_tls_key_create(&g_client_context_key, cleanup_client_context);
}
void set_client_context(int client_id, const char *name) {
client_context_t *ctx = malloc(sizeof(*ctx));
ctx->client_id = client_id;
ctx->name = strdup(name);
ascii_tls_set(g_client_context_key, ctx);
}
client_context_t* get_client_context(void) {
return (client_context_t *)ascii_tls_get(g_client_context_key);
}

Real-Time Priority

Set Realtime Priority:

asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
asciichat_error_t asciichat_thread_set_realtime_priority(void)
Set the current thread to real-time priority.

Elevate current thread to real-time priority.

  • Use case: Audio processing, video rendering, time-critical tasks
  • Return: ASCIICHAT_OK on success, error code on failure
  • Permissions:
    • Linux: Requires CAP_SYS_NICE capability or rtprio ulimit increase
    • macOS: Requires special mach thread setup
    • Windows: Requires no special permissions
  • Fallback: Always check return value; graceful degradation if not available

Audio Thread Setup:

void* audio_worker(void* arg) {
// Attempt to use real-time priority
log_warn("Could not enable real-time priority for audio thread");
} else {
log_debug("Audio thread running with real-time priority");
}
// Process audio in tight loop
while (!should_stop()) {
process_audio_frame();
}
return NULL;
}
atomic_t should_stop
Definition splash.c:97

Crash Handling

The platform abstraction layer automatically installs crash handlers that capture backtraces when your program crashes. This is invaluable for debugging!

Automatic Installation

Crash handlers are automatically installed when you call platform_init():

int main() {
platform_init(); // Installs crash handlers
// Your program runs...
platform_cleanup();
return 0;
}

No additional setup needed - it just works!

Supported Crash Types

POSIX (Linux/macOS):

  • SIGSEGV - Segmentation fault (null pointer, buffer overflow)
  • SIGABRT - Abort signal (assertion failures, abort() calls)
  • SIGFPE - Floating point exception (divide by zero)
  • SIGILL - Illegal instruction
  • SIGBUS - Bus error (alignment issues)

Windows:

  • EXCEPTION_ACCESS_VIOLATION - Access violation (like SIGSEGV)
  • EXCEPTION_ARRAY_BOUNDS_EXCEEDED - Array bounds exceeded
  • EXCEPTION_DATATYPE_MISALIGNMENT - Data type misalignment
  • EXCEPTION_FLT_DIVIDE_BY_ZERO - Floating point divide by zero
  • EXCEPTION_FLT_INVALID_OPERATION - Floating point invalid operation
  • EXCEPTION_ILLEGAL_INSTRUCTION - Illegal instruction
  • EXCEPTION_INT_DIVIDE_BY_ZERO - Integer divide by zero
  • EXCEPTION_STACK_OVERFLOW - Stack overflow
  • C runtime signals: SIGABRT, SIGFPE, SIGILL

Output Format

When a crash occurs, you get a detailed report:

*** CRASH DETECTED ***
Signal: SIGSEGV (Segmentation fault)
=== BACKTRACE ===
0: handle_client (lib/network.c:456)
1: client_thread (src/server.c:234)
2: thread_wrapper (lib/platform/posix/thread.c:89)
3: start_thread
================

The backtrace shows:

  • Function names (when debug symbols are available)
  • Source file and line number (with debug symbols)
  • Call stack from crash point to program entry

Symbol Resolution

Symbol resolution (function names in backtraces) works best with debug symbols:

Debug builds (recommended for development):

cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

Release builds (limited symbol info):

cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build

Platform-specific symbol support:

  • Linux: Uses backtrace_symbols() from execinfo.h
  • macOS: Uses backtrace_symbols() from execinfo.h
  • Windows: Uses StackWalk64() and SymFromAddr() with dbghelp.dll

Thread Safety

Crash handlers work across all threads in your application:

  • Windows: SetUnhandledExceptionFilter() is process-wide
  • POSIX: sigaction() with SA_SIGINFO works on all threads
  • Backtrace: Captures the call stack of the crashing thread

Example with multiple threads:

void* worker_thread(void* arg) {
int* ptr = NULL;
*ptr = 42; // CRASH! Null pointer dereference
return NULL;
}
int main() {
platform_init(); // Installs crash handlers
asciichat_thread_create(&thread, worker_thread, NULL);
asciichat_thread_join(&thread, NULL);
platform_cleanup();
return 0;
}
// Output:
// *** CRASH DETECTED ***
// Signal: SIGSEGV (Segmentation fault)
//
// === BACKTRACE ===
// 0: worker_thread
// 1: thread_wrapper
// 2: start_thread
// ================

The crash handler correctly identifies the crashing thread and its stack!

Windows-Specific Details

Windows has some unique characteristics that the platform layer handles for you.

Winsock Initialization

Windows requires explicit initialization of the Winsock library before using sockets:

// Handled automatically by platform_init()
int main() {
platform_init(); // Calls WSAStartup() on Windows
// Now you can use sockets!
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
platform_cleanup(); // Calls WSACleanup() on Windows
return 0;
}

You never need to call WSAStartup() or WSACleanup() directly!

ANSI Escape Sequences

Modern Windows (Windows 10+) supports ANSI escape sequences, but they need to be explicitly enabled:

// Handled automatically by platform_init()
int main() {
platform_init(); // Calls terminal_enable_ansi() on Windows
// Now you can use ANSI colors!
printf("\033[31mRed text\033[0m\n");
printf("\033[32mGreen text\033[0m\n");
platform_cleanup();
return 0;
}

The terminal functions automatically use Console API calls on older Windows versions.

Windows Lock Implementation

Windows uses different synchronization primitives than POSIX:

Mutexes (CRITICAL_SECTION):

// Implemented using CRITICAL_SECTION with spin count
typedef struct {
CRITICAL_SECTION cs;
InitializeCriticalSectionAndSpinCount(&mutex->cs, 4000);
mutex->initialized = true;
return 0;
}
bool initialized
Definition mmap.c:38

Spin count of 4000 means the thread will spin 4000 times before sleeping, which improves performance for short-held locks.

Read-Write Locks (SRWLOCK):

// Slim Reader-Writer Lock - only 8 bytes!
typedef struct {
SRWLOCK lock;
int rwlock_rdlock(rwlock_t* lock) {
AcquireSRWLockShared(&lock->lock);
return 0;
}
int rwlock_wrlock(rwlock_t* lock) {
AcquireSRWLockExclusive(&lock->lock);
return 0;
}

SRW Locks are lightweight (8 bytes vs 40+ bytes for CRITICAL_SECTION) and very fast.

Condition Variables:

typedef struct {
CONDITION_VARIABLE cv;
int cond_wait(cond_t* cond, mutex_t* mutex) {
SleepConditionVariableCS(&cond->cv, &mutex->cs, INFINITE);
return 0;
}

Windows Thread Implementation

The Windows thread implementation is the most complex part of the platform layer (2500+ lines!) because it includes sophisticated crash handling:

Thread Creation Flow:

  1. Allocate thread wrapper structure
  2. Initialize symbol handler (first thread only)
  3. Create Windows thread with wrapper function
  4. Wrapper catches exceptions and generates backtraces
  5. Call user's thread function
  6. Clean up on exit

Exception Handling in Threads:

DWORD WINAPI thread_wrapper(LPVOID arg) {
thread_wrapper_arg_t* wrapper_arg = (thread_wrapper_arg_t*)arg;
__try {
// Initialize symbol handler
ensure_symbol_handler_initialized();
__try {
// Call user's thread function
void* result = wrapper_arg->start_routine(wrapper_arg->arg);
return (DWORD)(uintptr_t)result;
}
__except(exception_filter(GetExceptionInformation())) {
// Inner exception handler
}
}
__finally {
// Cleanup
}
}

This nested exception handling ensures crashes are caught and reported even in worker threads!

Windows Signal Limitations

Windows has limited signal support compared to POSIX:

Working signals:

  • SIGINT - Ctrl+C (works via SetConsoleCtrlHandler)
  • SIGTERM - Termination request (limited support)
  • SIGABRT - Abort signal (via C runtime)
  • SIGFPE - Floating point exception (via C runtime)
  • SIGILL - Illegal instruction (via C runtime)

Non-working signals:

  • SIGWINCH - Terminal resize (defined but does nothing)
  • Most other POSIX signals

The platform layer provides these as no-ops so your code compiles:

// This compiles on Windows but does nothing
platform_signal(SIGWINCH, resize_handler);

Use Windows Console API functions like GetConsoleScreenBufferInfo() to detect terminal size changes on Windows.

Windows Backtrace Implementation

Windows backtraces use the DbgHelp library with multiple fallback strategies:

Primary method (StackWalk64):

STACKFRAME64 frame = {0};
frame.AddrPC.Offset = context.Rip; // Instruction pointer
frame.AddrStack.Offset = context.Rsp; // Stack pointer
frame.AddrFrame.Offset = context.Rbp; // Frame pointer
while (StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, thread,
&frame, &context, NULL, SymFunctionTableAccess64,
SymGetModuleBase64, NULL)) {
// Resolve symbol for frame.AddrPC.Offset
}

Fallback methods:

  1. Manual stack walking using frame pointers
  2. Symbol resolution using addr2line.exe
  3. Raw addresses if symbol resolution fails

This multi-strategy approach ensures you get the best possible backtrace even when debug symbols are missing or DbgHelp has issues.

POSIX-Specific Details

POSIX implementation is generally simpler because the platform abstraction API is based on POSIX semantics.

Thin Delegation

Most POSIX functions are thin wrappers:

// Thread creation - simple delegation
int asciichat_thread_create(asciichat_thread_t* thread, void* (*func)(void*), void* arg) {
return pthread_create(&thread->thread, NULL, func, arg);
}
// Mutex locking - simple delegation
return pthread_mutex_lock(&mutex->mutex);
}

This keeps the abstraction lightweight and efficient on POSIX platforms.

Full Signal Support

POSIX platforms have complete signal support:

// All signals work as expected
platform_signal(SIGINT, sigint_handler); // Ctrl+C
platform_signal(SIGTERM, sigterm_handler); // Termination
platform_signal(SIGWINCH, resize_handler); // Terminal resize
platform_signal(SIGUSR1, user_handler); // User-defined signal

The platform layer uses sigaction() for thread-safe signal handling:

struct sigaction new_action, old_action;
new_action.sa_handler = handler;
sigemptyset(&new_action.sa_mask);
new_action.sa_flags = SA_RESTART; // Restart interrupted syscalls
sigaction(sig, &new_action, &old_action);
return old_action.sa_handler;
}
void(* signal_handler_t)(int)
Definition misc.c:113

POSIX Backtrace

POSIX platforms use the standard execinfo.h backtrace API:

int platform_backtrace(void** buffer, int size) {
return backtrace(buffer, size);
}
char** platform_backtrace_symbols(void* const* buffer, int size) {
return backtrace_symbols(buffer, size);
}

Symbol resolution works automatically if:

  • Debug symbols are included in the binary
  • Binary is compiled with frame pointers (-fno-omit-frame-pointer)

macOS-Specific Notes

macOS is mostly POSIX-compliant but has a few quirks:

Thread timeout join: macOS doesn't have pthread_timedjoin_np(), so timeouts fall back to blocking:

int asciichat_thread_join_timeout(asciichat_thread_t* thread, void** retval, int timeout_ms) {
#ifdef __APPLE__
// No timeout support, fall back to blocking join
return pthread_join(thread->thread, retval);
#else
// Linux has pthread_timedjoin_np
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += timeout_ms / 1000;
ts.tv_nsec += (timeout_ms % 1000) * 1000000;
return pthread_timedjoin_np(thread->thread, retval, &ts);
#endif
}

Type name conflicts: macOS system headers define thread_t, so we use asciichat_thread_t to avoid conflicts:

// ❌ BAD - Conflicts with mach/mach_types.h
typedef struct { pthread_t thread; } thread_t;
// βœ… GOOD - Unique name avoids conflicts
typedef struct { pthread_t thread; } asciichat_thread_t;

Complete Examples

Here are some complete, real-world examples of using the platform abstraction layer.

Multi-Threaded Server

A simple echo server that handles multiple clients using threads:

#include <stdio.h>
#include <string.h>
typedef struct {
socket_t client_sock;
int client_id;
void* handle_client(void* arg) {
char buffer[1024];
printf("Client %d connected\n", info->client_id);
while (1) {
ssize_t received = socket_recv(info->client_sock, buffer, sizeof(buffer) - 1, 0);
if (received <= 0) break;
buffer[received] = '\0';
printf("Client %d: %s", info->client_id, buffer);
// Echo back to client
socket_send(info->client_sock, buffer, received, 0);
}
printf("Client %d disconnected\n", info->client_id);
socket_close(info->client_sock);
free(info);
return NULL;
}
int main() {
// Create listening socket
socket_t server_sock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_set_reuseaddr(server_sock, true);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(8080);
socket_bind(server_sock, (struct sockaddr*)&addr, sizeof(addr));
socket_listen(server_sock, 10);
printf("Server listening on port 8080\n");
// Accept clients and spawn threads
int client_id = 0;
while (1) {
socket_t client_sock = socket_accept(server_sock, NULL, NULL);
if (!socket_is_valid(client_sock)) continue;
client_info_t* info = malloc(sizeof(client_info_t));
info->client_sock = client_sock;
info->client_id = ++client_id;
if (asciichat_thread_create(&thread, handle_client, info) != 0) {
fprintf(stderr, "Failed to create thread\n");
socket_close(client_sock);
free(info);
}
}
socket_close(server_sock);
platform_cleanup();
return 0;
}
ssize_t socket_recv(socket_t sock, void *buf, size_t len, int flags)
Receive data from a socket.
struct client_info client_info_t
Per-client state structure for server-side client management.
Per-client state structure for server-side client management.
char client_id[MAX_CLIENT_ID_LEN]

Producer-Consumer Pattern

Classic producer-consumer with condition variables:

#include <stdio.h>
#include <stdlib.h>
#define QUEUE_SIZE 10
typedef struct {
int items[QUEUE_SIZE];
int count;
int head;
int tail;
mutex_t lock;
cond_t not_empty;
cond_t not_full;
} queue_t;
void queue_init(queue_t* q) {
q->count = 0;
q->head = 0;
q->tail = 0;
mutex_init(&q->lock);
cond_init(&q->not_empty);
cond_init(&q->not_full);
}
void queue_push(queue_t* q, int item) {
mutex_lock(&q->lock);
while (q->count == QUEUE_SIZE) {
cond_wait(&q->not_full, &q->lock);
}
q->items[q->tail] = item;
q->tail = (q->tail + 1) % QUEUE_SIZE;
q->count++;
cond_signal(&q->not_empty);
mutex_unlock(&q->lock);
}
int queue_pop(queue_t* q) {
mutex_lock(&q->lock);
while (q->count == 0) {
cond_wait(&q->not_empty, &q->lock);
}
int item = q->items[q->head];
q->head = (q->head + 1) % QUEUE_SIZE;
q->count--;
cond_signal(&q->not_full);
mutex_unlock(&q->lock);
return item;
}
void* producer(void* arg) {
queue_t* q = (queue_t*)arg;
for (int i = 0; i < 100; i++) {
queue_push(q, i);
printf("Produced: %d\n", i);
}
return NULL;
}
void* consumer(void* arg) {
queue_t* q = (queue_t*)arg;
for (int i = 0; i < 100; i++) {
int item = queue_pop(q);
printf("Consumed: %d\n", item);
}
return NULL;
}
int main() {
queue_t queue;
queue_init(&queue);
asciichat_thread_t prod_thread, cons_thread;
asciichat_thread_create(&prod_thread, producer, &queue);
asciichat_thread_create(&cons_thread, consumer, &queue);
asciichat_thread_join(&prod_thread, NULL);
asciichat_thread_join(&cons_thread, NULL);
mutex_destroy(&queue.lock);
cond_destroy(&queue.not_empty);
cond_destroy(&queue.not_full);
platform_cleanup();
return 0;
}

Interactive Terminal Application

Simple interactive menu using terminal I/O:

#include <stdio.h>
#include <unistd.h>
void display_menu() {
printf("╔════════════════════════════╗\n");
printf("β•‘ Main Menu β•‘\n");
printf("╠════════════════════════════╣\n");
printf("β•‘ 1. Option One β•‘\n");
printf("β•‘ 2. Option Two β•‘\n");
printf("β•‘ 3. Option Three β•‘\n");
printf("β•‘ Q. Quit β•‘\n");
printf("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n");
printf("\nChoice: ");
fflush(stdout);
}
int main() {
// Check if we're in a terminal
if (!platform_isatty(STDIN_FILENO)) {
fprintf(stderr, "This program must be run in a terminal\n");
return 1;
}
// Set up terminal
terminal_hide_cursor(true);
// Main loop
while (1) {
display_menu();
char choice;
read(STDIN_FILENO, &choice, 1);
if (choice == 'q' || choice == 'Q') {
break;
}
switch (choice) {
case '1':
printf("You selected option 1!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
case '2':
printf("You selected option 2!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
case '3':
printf("You selected option 3!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
}
}
// Restore terminal
terminal_hide_cursor(false);
platform_cleanup();
return 0;
}

Best Practices

Here are the golden rules for using the platform abstraction layer effectively.

Always Init and Cleanup

Rule: Always call platform_init() at program start and platform_cleanup() at exit.

// βœ… GOOD
int main() {
// Your program logic
platform_cleanup();
return 0;
}
// ❌ BAD - Missing init/cleanup
int main() {
socket_t sock = socket_create(...); // May fail on Windows!
return 0;
}

Why this matters:

  • Windows requires Winsock initialization before using sockets
  • Crash handlers are installed by platform_init()
  • ANSI terminal support is enabled by platform_init()
  • Proper cleanup prevents resource leaks

Never Use Platform Ifdefs

Rule: Never use #ifdef _WIN32 or #ifdef __linux__ in application code.

// ❌ BAD - Platform-specific code in application
#ifdef _WIN32
HANDLE thread;
CreateThread(...);
#else
pthread_t thread;
pthread_create(...);
#endif
// βœ… GOOD - Use platform abstraction
asciichat_thread_create(&thread, func, arg);

If you find yourself writing platform-specific code:

  1. Check if the platform layer already provides what you need
  2. If not, add it to the platform layer (don't scatter ifdefs!)
  3. Keep all platform differences isolated in lib/platform/

Use Platform Types

Rule: Use platform abstraction types, not native types.

// ❌ BAD - Platform-specific types
pthread_t thread; // Only works on POSIX!
CRITICAL_SECTION cs; // Only works on Windows!
int sockfd; // Wrong on Windows (should be SOCKET)!
// βœ… GOOD - Platform abstraction types
asciichat_thread_t thread; // Works everywhere
mutex_t mutex; // Works everywhere
socket_t sock; // Works everywhere

Platform types are opaque - you don't need to know (or care) what they contain.

Check Return Values

Rule: Always check return values from platform functions.

// ❌ BAD - No error checking
asciichat_thread_create(&thread, func, arg);
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind(sock, &addr, sizeof(addr));
// βœ… GOOD - Proper error checking
if (asciichat_thread_create(&thread, func, arg) != 0) {
log_error("Thread creation failed");
return ERROR_THREAD_CREATE;
}
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
if (!socket_is_valid(sock)) {
log_error("Socket creation failed: %s", socket_get_error_string());
return ERROR_SOCKET_CREATE;
}
if (socket_bind(sock, &addr, sizeof(addr)) < 0) {
log_error("Bind failed: %s", socket_get_error_string());
socket_close(sock);
return ERROR_SOCKET_BIND;
}

Platform functions return:

  • 0 on success, non-zero on error (for most functions)
  • INVALID_SOCKET_VALUE for invalid sockets (use socket_is_valid())
  • NULL for failed allocations

Prefer Static Init

Rule: Use static initialization for global synchronization primitives.

// ❌ BAD - Explicit initialization required
static mutex_t g_mutex;
static bool g_mutex_initialized = false;
void init() {
if (!g_mutex_initialized) {
mutex_init(&g_mutex);
g_mutex_initialized = true;
}
}
void use_mutex() {
if (!g_mutex_initialized) init();
mutex_lock(&g_mutex);
// ...
}
// βœ… GOOD - Static initialization, no init needed
void use_mutex() {
static_mutex_lock(&g_mutex); // Just works!
// ...
static_mutex_unlock(&g_mutex);
}

Static initialization is:

  • Thread-safe: No race conditions
  • Automatic: No explicit init calls needed
  • Clean: Less boilerplate code

Check Thread Creation Before Join

Rule: Only join threads that were successfully created.

// ❌ BAD - Joining uninitialized thread
asciichat_thread_create(&thread, func, arg); // What if this fails?
asciichat_thread_join(&thread, NULL); // Undefined behavior!
// βœ… GOOD - Check creation before join
bool thread_created = false;
if (asciichat_thread_create(&thread, func, arg) == 0) {
} else {
log_error("Thread creation failed");
}
asciichat_thread_join(&thread, NULL);
}
atomic_t thread_created
Definition splash.c:98

Or use a helper function:

// Check if thread was initialized
asciichat_thread_join(&thread, NULL);
}

Socket Validation

Rule: Use socket_is_valid() to check socket validity, not comparisons.

// ❌ BAD - Platform-specific checks
if (sock < 0) { ... } // Wrong on Windows!
if (sock == NULL) { ... } // Wrong on POSIX!
if (sock >= 0) { ... } // Wrong on Windows!
// βœ… GOOD - Platform-independent check
if (!socket_is_valid(sock)) {
log_error("Invalid socket");
}
if (socket_is_valid(sock)) {
// Socket is valid, use it
}

Why this matters:

  • POSIX: Invalid sockets are -1, valid sockets are >= 0
  • Windows: Invalid sockets are INVALID_SOCKET (typically ~0), valid sockets can be any value

Migration Guide

Converting existing code to use the platform abstraction layer is straightforward.

Migrating Threads

From POSIX pthreads:

// Before (POSIX-only)
#include <pthread.h>
pthread_t thread;
pthread_create(&thread, NULL, worker, arg);
pthread_join(thread, NULL);
// After (cross-platform)
asciichat_thread_create(&thread, worker, arg);
asciichat_thread_join(&thread, NULL);

From Windows threads:

// Before (Windows-only)
#include <windows.h>
HANDLE thread = CreateThread(NULL, 0, worker, arg, 0, NULL);
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
// After (cross-platform)
asciichat_thread_create(&thread, worker, arg);
asciichat_thread_join(&thread, NULL);

Migrating Mutexes

From POSIX mutexes:

// Before
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
// After

From Windows critical sections:

// Before
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
// After

Migrating Sockets

From POSIX sockets:

// Before
#include <sys/socket.h>
#include <netinet/in.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, &addr, sizeof(addr));
listen(sockfd, 10);
int client = accept(sockfd, NULL, NULL);
close(sockfd);
// After
socket_t sockfd = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind(sockfd, &addr, sizeof(addr));
socket_listen(sockfd, 10);
socket_t client = socket_accept(sockfd, NULL, NULL);
socket_close(sockfd);

From Windows Winsock:

// Before
#include <winsock2.h>
WSADATA wsa;
WSAStartup(MAKEWORD(2,2), &wsa);
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, &addr, sizeof(addr));
listen(sockfd, 10);
SOCKET client = accept(sockfd, NULL, NULL);
closesocket(sockfd);
WSACleanup();
// After
platform_init(); // Handles WSAStartup
socket_t sockfd = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind(sockfd, &addr, sizeof(addr));
socket_listen(sockfd, 10);
socket_t client = socket_accept(sockfd, NULL, NULL);
socket_close(sockfd);
platform_cleanup(); // Handles WSACleanup

Migrating Terminal I/O

From termios (POSIX):

// Before
#include <termios.h>
struct termios old_tio, new_tio;
tcgetattr(STDIN_FILENO, &old_tio);
new_tio = old_tio;
new_tio.c_lflag &= ~(ICANON | ECHO);
tcsetattr(STDIN_FILENO, TCSANOW, &new_tio);
// ...
tcsetattr(STDIN_FILENO, TCSANOW, &old_tio);
// After
// ...

Migrating Signal Handling

From POSIX signals:

// Before
#include <signal.h>
void handler(int sig) { /* ... */ }
signal(SIGINT, handler);
// After
void handler(int sig) { /* ... */ }
platform_signal(SIGINT, handler); // Thread-safe on all platforms!
Cross-platform crash signal and exception handling.

Troubleshooting

Common issues and how to solve them.

Link Errors

Problem: Undefined reference to platform functions.

Solution: Make sure platform files are included in your build:

# CMakeLists.txt
if(WIN32)
set(PLATFORM_SOURCES
lib/platform/windows/thread.c
lib/platform/windows/mutex.c
# ... other Windows files
)
else()
set(PLATFORM_SOURCES
lib/platform/posix/thread.c
lib/platform/posix/mutex.c
# ... other POSIX files
)
endif()
add_executable(my_app main.c ${PLATFORM_SOURCES})

Socket Creation Fails

Problem: socket_create() returns invalid socket on Windows.

Solution: Call platform_init() before using sockets:

int main() {
platform_init(); // Must be called first on Windows!
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
// Now it works!
platform_cleanup();
return 0;
}

Thread Join Crashes

Problem: Crash when calling asciichat_thread_join().

Solution: Check if thread was successfully created:

if (asciichat_thread_create(&thread, func, arg) == 0) {
// Only join if creation succeeded
asciichat_thread_join(&thread, NULL);
}

No Function Names in Backtrace

Problem: Backtrace shows addresses but no function names.

Solution: Build with debug symbols:

# Enable debug symbols
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build

On Linux, also install debug info:

# Debian/Ubuntu
sudo apt-get install libc6-dbg

Timeout Functions Don't Work on Windows

Problem: Timeout functions seem to block forever on Windows.

Solution: This is expected - some timeout functions fall back to blocking on certain platforms. Use a separate watchdog thread if you need guaranteed timeouts:

// Watchdog pattern for guaranteed timeout
typedef struct {
bool completed;
mutex_t lock;
} watchdog_t;
void* watchdog_func(void* arg) {
watchdog_t* wd = (watchdog_t*)arg;
platform_sleep_ms(5000); // 5 second timeout
mutex_lock(&wd->lock);
if (!wd->completed) {
log_warn("Thread did not complete in time!");
// Handle timeout
}
mutex_unlock(&wd->lock);
return NULL;
}

Performance Considerations

The platform abstraction layer is designed for minimal overhead, but there are still some performance characteristics to be aware of.

Zero Overhead in Release

Release builds have essentially zero abstraction overhead:

  • Direct function calls (no virtual dispatch)
  • Inlined wrappers for simple operations
  • Compile-time platform selection

Example assembly comparison (mutex lock on Linux):

; Direct pthread call
call pthread_mutex_lock
; Platform abstraction call (same!)
call mutex_lock
; mutex_lock implementation:
jmp pthread_mutex_lock ; Direct jump, no overhead

Debug Mode Overhead

Debug builds can enable tracking that has some overhead:

  • Lock tracking: ~5-10% overhead
  • Thread tracking: ~2-5% overhead
  • Memory tracking: ~10-20% overhead

This is acceptable in debug builds because:

  • You get valuable debugging information
  • Debug builds aren't used in production
  • The overhead helps find bugs early

Socket Auto-Optimization

The platform layer automatically optimizes accepted sockets:

  • TCP_NODELAY: Disables Nagle algorithm for low latency
  • Large buffers: 2MB send/recv buffers (falls back to 512KB, 128KB)
  • Timeouts: 5s send, 10s recv timeouts prevent hanging
  • Keepalive: Detects broken connections

These optimizations are specifically tuned for ascii-chat's video streaming use case. If you need different settings, you can override them:

socket_t client = socket_accept(server, NULL, NULL);
// Override auto-optimization
socket_set_nodelay(client, false); // Re-enable Nagle
socket_setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, ...); // Custom timeout

Sleep Precision

Platform sleep precision varies:

  • Linux: Microsecond precision with nanosleep()
  • macOS: Microsecond precision with nanosleep()
  • Windows: ~15ms minimum resolution (system timer granularity)

For Windows, use multimedia timers if you need better precision:

#ifdef _WIN32
timeBeginPeriod(1); // Request 1ms timer resolution
platform_sleep_ms(10); // Now actually sleeps ~10ms
timeEndPeriod(1); // Restore default resolution
#else
platform_sleep_ms(10); // Already precise
#endif

Contributing

Want to add new platform abstractions or improve existing ones?

Adding New Platform APIs

When adding a new platform abstraction:

  1. Define the interface in a new header (e.g., lib/platform/foo.h):
    #ifndef PLATFORM_FOO_H
    #define PLATFORM_FOO_H
    #include "platform/util.h"
    typedef struct foo_s foo_t;
    int foo_init(foo_t* foo);
    #endif
  2. Implement for POSIX (lib/platform/posix/foo.c):
    #include "platform/foo.h"
    #include <posix_foo.h> // POSIX-specific header
    struct foo_s {
    posix_foo_t native_foo;
    };
    int foo_init(foo_t* foo) {
    return posix_foo_init(&foo->native_foo);
    }
  3. Implement for Windows (lib/platform/windows/foo.c):
    #include "platform/foo.h"
    #include <windows.h>
    struct foo_s {
    HANDLE native_foo;
    };
    int foo_init(foo_t* foo) {
    foo->native_foo = CreateFoo(...);
    return (foo->native_foo != NULL) ? 0 : -1;
    }
  4. Add to abstraction.h:
    #include "platform/foo.h"
  5. Document thoroughly with Doxygen comments
  6. Write tests for all platforms

Writing Platform Tests

Platform abstraction tests should:

  • Test on all platforms (Windows, Linux, macOS)
  • Test edge cases and error conditions
  • Verify thread safety where applicable
  • Use Criterion test framework

Example test structure:

#include <criterion/criterion.h>
#include "platform/foo.h"
Test(foo, initialization) {
foo_t foo;
cr_assert_eq(foo_init(&foo), 0, "foo_init should succeed");
foo_destroy(&foo);
}
Test(foo, thread_safety) {
// Test concurrent access from multiple threads
}

Additional Resources

Want to learn more?

Platform Abstraction Documentation:

  • Platform README - Original documentation
  • abstraction.h - Main header file

Individual Component Documentation:

  • thread.h - Threading API
  • mutex.h - Mutex API
  • rwlock.h - Read-write lock API
  • cond.h - Condition variable API
  • socket.h - Socket API
  • terminal.h - Terminal I/O API
  • system.h - System functions API
  • string.h - String operations API
  • file.h - File I/O API
  • init.h - Static initialization API

External Resources:

Related Topics:

Author
Zachary Fogg me@zf.nosp@m.o.gg
Date
September 2025

Webcam Capture

Cross-platform webcam capture functionality for Windows, Linux, and macOS.

Overview

The webcam module (lib/video/webcam/) provides cross-platform webcam capture with platform-specific implementations:

  • Webcam capture: Different APIs per platform (AVFoundation, V4L2, Media Foundation)

Platform Implementations

Each platform has its own webcam API with different capabilities and performance:

macOS - AVFoundation

Implementation: lib/video/webcam/macos/webcam_avfoundation.m (Objective-C)

API: AVFoundation framework (modern, recommended by Apple)

  • AVCaptureDevice: Represents physical camera
  • AVCaptureSession: Manages capture pipeline
  • AVCaptureVideoDataOutput: Provides frame callbacks

Capabilities:

  • Supports all built-in and USB webcams
  • Hardware-accelerated video processing
  • Multiple simultaneous capture sessions
  • Automatic format negotiation
  • Native RGB/YUV pixel formats

Supported Resolutions:

  • 640x480 @ 30 FPS (VGA, default)
  • 1280x720 @ 30 FPS (HD)
  • 1920x1080 @ 30 FPS (Full HD, if supported)
  • 3840x2160 @ 30 FPS (4K, if supported)

Pixel Formats:

  • kCVPixelFormatType_24RGB: 24-bit RGB (preferred)
  • kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange: NV12 (efficient)
  • Automatic conversion handled by Core Video

Performance:

  • ~5% CPU for 1920x1080 @ 30 FPS capture
  • Zero-copy frame access via CVPixelBuffer
  • Hardware-accelerated color space conversion

Implementation Example:

// Create capture session
AVCaptureSession *session = [[AVCaptureSession alloc] init];
session.sessionPreset = AVCaptureSessionPreset1280x720;
// Get default camera
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:nil];
[session addInput:input];
// Configure output
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
output.videoSettings = @{
(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_24RGB)
};
// Set frame callback
dispatch_queue_t queue = dispatch_queue_create("webcam", NULL);
[output setSampleBufferDelegate:self queue:queue];
[session addOutput:output];
// Start capture
[session startRunning];

Linux - Video4Linux2 (V4L2)

Implementation: lib/video/webcam/linux/webcam_v4l2.c (C)

API: Video4Linux2 kernel interface (standard Linux video API)

  • Device nodes: /dev/video0, /dev/video1, etc.
  • ioctl-based control interface
  • Memory-mapped I/O for zero-copy capture
  • Support for USB webcams and built-in cameras

Capabilities:

  • Universal Linux webcam support
  • Low-level control over camera settings
  • Efficient memory-mapped buffers (4-8 buffers typical)
  • Select/poll support for async I/O
  • Multiple pixel format support

Supported Pixel Formats:

  • V4L2_PIX_FMT_RGB24: 24-bit RGB (preferred)
  • V4L2_PIX_FMT_YUYV: YUV 4:2:2 (common, requires conversion)
  • V4L2_PIX_FMT_MJPEG: Motion JPEG (requires decoding)
  • V4L2_PIX_FMT_H264: H.264 compressed (requires decoding)

Device Enumeration:

// Find all video devices
for (int i = 0; i < 64; i++) {
char devname[32];
snprintf(devname, sizeof(devname), "/dev/video%d", i);
int fd = open(devname, O_RDWR);
if (fd >= 0) {
struct v4l2_capability cap;
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == 0) {
if (cap.capabilities & V4L2_CAP_VIDEO_CAPTURE) {
log_info("Found camera: %s (%s)", cap.card, devname);
}
}
close(fd);
}
}

Memory-Mapped Capture:

// Request buffers
struct v4l2_requestbuffers req = {0};
req.count = 4; // Number of buffers
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_REQBUFS, &req);
// Map buffers
struct buffer {
void *start;
size_t length;
} buffers[4];
for (int i = 0; i < req.count; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(fd, VIDIOC_QUERYBUF, &buf);
buffers[i].length = buf.length;
buffers[i].start = mmap(NULL, buf.length,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, buf.m.offset);
}
// Start capture
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(fd, VIDIOC_STREAMON, &type);
// Dequeue frame
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
ioctl(fd, VIDIOC_DQBUF, &buf);
// Process frame at buffers[buf.index].start
// ...
// Requeue buffer
ioctl(fd, VIDIOC_QBUF, &buf);
platform_mmap_t mmap
Definition mmap.c:34

Performance:

  • ~3% CPU for 1920x1080 @ 30 FPS capture (RGB)
  • Zero-copy via memory mapping
  • ~10% CPU if YUYVβ†’RGB conversion needed

Windows - Media Foundation

Implementation: lib/video/webcam/windows/webcam_mediafoundation.c (C++)

API: Media Foundation (modern Windows multimedia API, Vista+)

  • IMFMediaSource: Represents camera device
  • IMFSourceReader: Simplified capture interface
  • COM-based object model

Capabilities:

  • Supports all UVC (USB Video Class) webcams
  • Built-in Windows camera app compatibility
  • Hardware-accelerated video processing
  • Automatic format negotiation
  • Native RGB/YUV pixel formats

Supported Pixel Formats:

  • MFVideoFormat_RGB24: 24-bit RGB (preferred)
  • MFVideoFormat_NV12: NV12 YUV (efficient, requires conversion)
  • MFVideoFormat_YUY2: YUY2 422 (common)
  • MFVideoFormat_MJPG: Motion JPEG (requires decoding)

Device Enumeration:

// Initialize COM
CoInitializeEx(NULL, COINIT_MULTITHREADED);
MFStartup(MF_VERSION);
// Enumerate video capture devices
IMFAttributes *attributes = NULL;
MFCreateAttributes(&attributes, 1);
attributes->SetGUID(MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID);
IMFActivate **devices = NULL;
UINT32 count = 0;
MFEnumDeviceSources(attributes, &devices, &count);
for (UINT32 i = 0; i < count; i++) {
WCHAR *friendly_name = NULL;
UINT32 name_len = 0;
devices[i]->GetAllocatedString(MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
&friendly_name, &name_len);
log_info("Found camera: %ls", friendly_name);
CoTaskMemFree(friendly_name);
}

Capture with Source Reader:

// Activate device
IMFMediaSource *source = NULL;
devices[0]->ActivateObject(IID_PPV_ARGS(&source));
// Create source reader
IMFSourceReader *reader = NULL;
MFCreateSourceReaderFromMediaSource(source, NULL, &reader);
// Configure output format
IMFMediaType *media_type = NULL;
MFCreateMediaType(&media_type);
media_type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
media_type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_RGB24);
MFSetAttributeSize(media_type, MF_MT_FRAME_SIZE, 1920, 1080);
reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_VIDEO_STREAM, NULL, media_type);
// Read frame
IMFSample *sample = NULL;
DWORD stream_flags = 0;
reader->ReadSample(MF_SOURCE_READER_FIRST_VIDEO_STREAM, 0,
NULL, &stream_flags, NULL, &sample);
if (sample) {
IMFMediaBuffer *buffer = NULL;
sample->ConvertToContiguousBuffer(&buffer);
BYTE *data = NULL;
DWORD length = 0;
buffer->Lock(&data, NULL, &length);
// Process frame data...
buffer->Unlock();
buffer->Release();
sample->Release();
}

Performance:

  • ~4% CPU for 1920x1080 @ 30 FPS capture (RGB)
  • Hardware-accelerated decoding for MJPEG/H.264

Unified Webcam API

ascii-chat provides a unified C API across all platforms:

webcam_context_t *ctx = NULL;
if (result != ASCIICHAT_OK) {
return result;
}
while (running) {
if (frame) {
process_frame(frame->pixels, frame->w, frame->h);
}
}
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
struct webcam_context_t webcam_context_t
Opaque webcam context structure.
Definition webcam.h:150
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
Image structure.

Hardware Detection

CPU capability detection for SIMD optimization:

x86/x86_64:

bool cpu_has_sse2(void); // SSE2 (baseline for x86_64)
bool cpu_has_ssse3(void); // SSSE3
bool cpu_has_avx2(void); // AVX2
bool cpu_has_aes(void); // AES-NI instructions
bool cpu_has_crc32(void); // CRC32 instruction

Implementation uses CPUID instruction:

static void cpuid(uint32_t leaf, uint32_t *eax, uint32_t *ebx,
uint32_t *ecx, uint32_t *edx) {
__asm__ __volatile__(
"cpuid"
: "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx)
: "a"(leaf)
);
}
bool cpu_has_avx2(void) {
uint32_t eax, ebx, ecx, edx;
cpuid(7, &eax, &ebx, &ecx, &edx);
return (ebx & (1 << 5)) != 0; // AVX2 bit
}
unsigned int uint32_t
Definition common.h:58

ARM/ARM64:

bool cpu_has_neon(void); // NEON SIMD
bool cpu_has_sve(void); // Scalable Vector Extension
bool cpu_has_crypto(void); // ARM Crypto Extensions

Implementation reads /proc/cpuinfo on Linux, sysctlbyname on macOS:

#ifdef __APPLE__
bool cpu_has_neon(void) {
// All Apple Silicon has NEON
return true;
}
#elif defined(__linux__)
bool cpu_has_neon(void) {
FILE *f = fopen("/proc/cpuinfo", "r");
char line[256];
while (fgets(line, sizeof(line), f)) {
if (strstr(line, "Features") && strstr(line, "neon")) {
fclose(f);
return true;
}
}
fclose(f);
return false;
}
#endif

Platform-Specific Optimizations

Windows:

  • SetThreadPriority for realtime video/audio threads
  • SetProcessPriorityBoost to disable dynamic priority boost
  • QueryPerformanceCounter for high-resolution timestamps
  • Multimedia Class Scheduler Service (MMCSS) for low-latency audio

macOS:

  • pthread_setschedparam for realtime threads
  • mach_timebase_info for nanosecond timing
  • Metal acceleration for video processing (future)
  • Core Audio low-latency mode

Linux:

  • SCHED_FIFO for realtime scheduling (requires CAP_SYS_NICE)
  • clock_gettime(CLOCK_MONOTONIC) for precise timing
  • memfd_create for anonymous shared memory
  • ALSA period size tuning for low-latency audio
See also
video/webcam/webcam.h
video/webcam/macos/webcam_avfoundation.m
video/webcam/linux/webcam_v4l2.c
video/webcam/windows/webcam_mediafoundation.c