ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
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

Welcome! This guide will help you understand the platform abstraction layerβ€”the unsung hero that makes ascii-chat work seamlessly across Windows, Linux, and macOS.

Here's the thing about cross-platform development: every operating system has its own way of doing things. Windows uses different APIs than Linux. macOS has its quirks. Writing code that works everywhere usually means lots of #ifdef _WIN32 scattered throughout your codebase, making it a mess to read and maintain.

But what if you could write your code once and have it just work everywhere? That's exactly what the platform abstraction layer does. It provides a unified APIβ€”one set of functions that work identically on all platforms. Under the hood, it translates your calls to the appropriate platform-specific APIs. You write clean, readable code, and the platform layer handles all the messy details.

Think of it like an adapter plug for international travel. You don't need to carry different devices for each countryβ€”you just bring one adapter that works everywhere. Same idea here!

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_t thread;
asciichat_thread_create(&thread, thread_func, arg);
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
Definition threading.c:42

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);

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)
int mutex_lock(mutex_t *mutex) {
return pthread_mutex_lock(&mutex->mutex);
}
// Windows version (adaptation)
int mutex_lock(mutex_t *mutex) {
EnterCriticalSection(&mutex->cs);
return 0; // Windows doesn't return error codes like POSIX
}

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
} asciichat_thread_t;
// Application just uses asciichat_thread_t
asciichat_thread_t my_thread;
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
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
void critical_function() {
static_mutex_lock(&g_mutex);
// Critical section
static_mutex_unlock(&g_mutex);
}

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

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

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:

#include "platform/abstraction.h"
void* worker_thread(void* arg) {
int* value = (int*)arg;
printf("Worker processing: %d\n", *value);
return NULL;
}
int main() {
// Create thread
asciichat_thread_t 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;
}
asciichat_error_t platform_init(void)
Definition init.c:10
int main(int argc, char *argv[])
Definition main.c:281
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Definition threading.c:46

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");
}

Thread identity and comparison:

// Get current thread ID
thread_id_t my_id = asciichat_thread_self();
// Compare thread IDs
if (asciichat_thread_equal(my_id, other_id)) {
log_debug("Same thread");
}
// Get numeric thread ID for logging
uint64_t tid = asciichat_thread_current_id();
log_debug("Thread ID: %" PRIu64, tid);
asciichat_thread_t asciichat_thread_self(void)
Definition threading.c:54
int asciichat_thread_equal(asciichat_thread_t t1, asciichat_thread_t t2)
Definition threading.c:58
uint64_t asciichat_thread_current_id(void)
Definition threading.c:84

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:

#include "platform/abstraction.h"
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)
Definition threading.c:16
int mutex_destroy(mutex_t *mutex)
Definition threading.c:21

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");
}

Static initialization for global mutexes:

// Global mutex - no explicit init needed!
static_mutex_t g_config_lock = STATIC_MUTEX_INIT;
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:

#include "platform/abstraction.h"
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);
}

Real-world example from ascii-chat:

// Global client list protected by read-write lock
static_rwlock_t g_client_manager_rwlock = STATIC_RWLOCK_INIT;
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 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.

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:

#include "platform/abstraction.h"
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);
}

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);

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);

Static initialization:

// Global condition variable
static_cond_t g_shutdown_cond = STATIC_COND_INIT;
static_mutex_t g_shutdown_lock = STATIC_MUTEX_INIT;
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.

Sockets (socket.h)

Network socket operations with automatic optimization and error handling.

Basic server:

#include "platform/abstraction.h"
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_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

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) {
if (socket_get_last_error() == EWOULDBLOCK) {
// Would block, try again later
}
}

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
}
}

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:

#include "platform/abstraction.h"
int main() {
// Get terminal size
terminal_size_t size;
if (terminal_get_size(&size) == 0) {
printf("Terminal: %dx%d\n", size.cols, size.rows);
}
// Check capabilities
if (terminal_supports_color()) {
printf("Terminal supports color!\n");
}
if (terminal_supports_unicode()) {
printf("Terminal supports Unicode!\n");
}
platform_cleanup();
return 0;
}
asciichat_error_t terminal_get_size(terminal_size_t *size)

Raw mode for interactive applications:

// Enable raw mode (no line buffering, no echo)
terminal_set_raw_mode(true);
terminal_set_echo(false);
// Now read individual keypresses
char c;
read(STDIN_FILENO, &c, 1);
printf("You pressed: %c\n", c);
// Restore normal mode
terminal_set_raw_mode(false);
terminal_set_echo(true);

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
terminal_save_cursor();
terminal_move_cursor(1, 1);
printf("Hello from top-left!");
terminal_restore_cursor(); // Back to original position
// Show cursor again
terminal_hide_cursor(false);

Screen manipulation:

// Clear entire screen
terminal_clear_screen();
// Set terminal title
terminal_set_title("ascii-chat v1.0");
// Ring the terminal bell
terminal_ring_bell();
// Set scroll region (for double-buffering)
terminal_set_scroll_region(5, 20); // Rows 5-20 can scroll
// Reset terminal to default state
terminal_reset();

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)

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)
Definition wasm/system.c:35

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)
Definition wasm/system.c:24
const char * platform_getenv(const char *name)
Definition wasm/system.c:13

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");
}
int platform_isatty(int fd)
Definition util.c:61

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);

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)
Definition util.c:14
void platform_print_backtrace(int skip_frames)
Definition util.c:42
char ** platform_backtrace_symbols(void *const *buffer, int size)
Definition util.c:20

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 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)
int platform_strcasecmp(const char *s1, const char *s2)

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)

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);
}

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 *pathname, int flags,...)

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));
}
_Atomic uint64_t bytes_written
Definition mmap.c:42
ssize_t platform_read(int fd, void *buf, size_t count)
ssize_t platform_write(int fd, const void *buf, size_t count)

Closing files:

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

Force sync to disk:

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

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!
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
void use_mutex() {
static_mutex_lock(&g_mutex); // Just works!
// ...
static_mutex_unlock(&g_mutex);
}

How It Works

The magic happens at different times on different platforms:

POSIX (Linux/macOS):

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

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_mutex_t g_config_mutex = STATIC_MUTEX_INIT;
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);
}

Global read-write lock:

static_rwlock_t g_cache_lock = STATIC_RWLOCK_INIT;
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_cond_t g_work_cond = STATIC_COND_INIT;
static_mutex_t g_work_mutex = STATIC_MUTEX_INIT;
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);
}

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_t thread;
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;
} mutex_t;
int mutex_init(mutex_t* mutex) {
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;
} rwlock_t;
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;
} cond_t;
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
int mutex_lock(mutex_t* mutex) {
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:

signal_handler_t platform_signal(int sig, signal_handler_t handler) {
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;
}

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 "platform/abstraction.h"
#include <stdio.h>
#include <string.h>
typedef struct {
socket_t client_sock;
int client_id;
} client_info_t;
void* handle_client(void* arg) {
client_info_t* info = (client_info_t*)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;
asciichat_thread_t thread;
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;
}

Producer-Consumer Pattern

Classic producer-consumer with condition variables:

#include "platform/abstraction.h"
#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 "platform/abstraction.h"
#include <stdio.h>
#include <unistd.h>
void display_menu() {
terminal_clear_screen();
terminal_move_cursor(1, 1);
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_set_raw_mode(true);
terminal_set_echo(false);
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':
terminal_clear_screen();
printf("You selected option 1!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
case '2':
terminal_clear_screen();
printf("You selected option 2!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
case '3':
terminal_clear_screen();
printf("You selected option 3!\n");
printf("Press any key to continue...");
read(STDIN_FILENO, &choice, 1);
break;
}
}
// Restore terminal
terminal_hide_cursor(false);
terminal_set_raw_mode(false);
terminal_set_echo(true);
terminal_clear_screen();
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_t thread;
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
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
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_t thread;
asciichat_thread_create(&thread, func, arg); // What if this fails?
asciichat_thread_join(&thread, NULL); // Undefined behavior!
// βœ… GOOD - Check creation before join
asciichat_thread_t thread;
bool thread_created = false;
if (asciichat_thread_create(&thread, func, arg) == 0) {
thread_created = true;
} else {
log_error("Thread creation failed");
}
if (thread_created) {
asciichat_thread_join(&thread, NULL);
}

Or use a helper function:

// Check if thread was initialized
if (asciichat_thread_is_initialized(&thread)) {
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)
#include "platform/abstraction.h"
asciichat_thread_t thread;
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)
#include "platform/abstraction.h"
asciichat_thread_t thread;
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
mutex_t mutex;
mutex_init(&mutex);
mutex_lock(&mutex);
mutex_unlock(&mutex);
mutex_destroy(&mutex);

From Windows critical sections:

// Before
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
// After
mutex_t mutex;
mutex_init(&mutex);
mutex_lock(&mutex);
mutex_unlock(&mutex);
mutex_destroy(&mutex);

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
#include "platform/abstraction.h"
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
#include "platform/abstraction.h"
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
#include "platform/abstraction.h"
terminal_set_raw_mode(true);
terminal_set_echo(false);
// ...
terminal_set_raw_mode(false);
terminal_set_echo(true);

Migrating Signal Handling

From POSIX signals:

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

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:

asciichat_thread_t thread;
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 {
asciichat_thread_t thread;
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:

typedef struct webcam_context webcam_context_t;
// Initialize webcam
webcam_context_t *webcam_init(int device_index, int width, int height, int fps);
// Start capture
asciichat_error_t webcam_start(webcam_context_t *ctx);
// Capture frame (blocking)
asciichat_error_t webcam_capture_frame(webcam_context_t *ctx,
uint8_t **rgb_data,
size_t *data_len);
// Stop capture
asciichat_error_t webcam_stop(webcam_context_t *ctx);
// Cleanup
void webcam_destroy(webcam_context_t *ctx);
// List available devices
char** webcam_list_devices(int *count);
void webcam_destroy(void)
asciichat_error_t webcam_init(unsigned short int webcam_index)
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)

Usage example:

// Initialize webcam (first device, 1920x1080 @ 30fps)
webcam_context_t *webcam = webcam_init(0, 1920, 1080, 30);
if (!webcam) {
log_error("Failed to initialize webcam");
return ERROR_WEBCAM_INIT;
}
// Start capture
webcam_start(webcam);
// Capture loop
while (running) {
uint8_t *rgb_frame;
size_t frame_len;
asciichat_error_t err = webcam_capture_frame(webcam, &rgb_frame, &frame_len);
if (err == ASCIICHAT_OK) {
// Process frame...
process_frame(rgb_frame, 1920, 1080);
}
}
// Cleanup
webcam_stop(webcam);

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
}

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