Platform-independent APIs for Windows, Linux, and macOS.
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:
#ifdef _WIN32
HANDLE thread;
CreateThread(...);
#else
pthread_t thread;
pthread_create(...);
#endif
asciichat_thread_t thread;
int asciichat_thread_create(asciichat_thread_t *thread, void *(*start_routine)(void *), void *arg)
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:
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:
#define DEBUG_THREADS
#define DEBUG_LOCKS
#define DEBUG_MEMORY
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:
- Defines API based on POSIX semantics
- POSIX implementation is thin wrapper (often just delegation)
- Windows implementation adapts Windows APIs to POSIX semantics
Example:
int mutex_lock(mutex_t *mutex) {
return pthread_mutex_lock(&mutex->mutex);
}
int mutex_lock(mutex_t *mutex) {
EnterCriticalSection(&mutex->cs);
return 0;
}
Strong Type Safety
Principle: Use distinct types to prevent platform-specific mistakes.
Platform types are completely opaque to application code:
typedef struct {
#ifdef _WIN32
HANDLE handle;
#else
pthread_t thread;
#endif
} asciichat_thread_t;
asciichat_thread_t my_thread;
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:
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
void critical_function() {
static_mutex_lock(&g_mutex);
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
Header Organization
Most application code only needs to include one header:
#include "platform/abstraction.h"
For specific components, you can include individual headers:
#include "platform/thread.h"
#include "platform/socket.h"
#include "platform/terminal.h"
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;
}
asciichat_thread_t thread;
int data = 42;
if (result != 0) {
log_error("Thread creation failed");
return 1;
}
platform_cleanup();
return 0;
}
asciichat_error_t platform_init(void)
int main(int argc, char *argv[])
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Thread operations with timeout:
int result = asciichat_thread_join_timeout(&thread, NULL, 5000);
if (result == ETIMEDOUT) {
log_warn("Thread did not finish in time");
} else if (result == 0) {
log_info("Thread completed successfully");
}
Thread identity and comparison:
log_debug("Same thread");
}
log_debug("Thread ID: %" PRIu64, tid);
asciichat_thread_t asciichat_thread_self(void)
int asciichat_thread_equal(asciichat_thread_t t1, asciichat_thread_t t2)
uint64_t asciichat_thread_current_id(void)
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);
}
shared_data_t data;
data.counter = 0;
return 0;
}
int mutex_init(mutex_t *mutex)
int mutex_destroy(mutex_t *mutex)
Try-lock (non-blocking):
int result = mutex_trylock(&data->lock);
if (result == 0) {
data->counter++;
mutex_unlock(&data->lock);
} else {
log_debug("Lock busy, skipping operation");
}
Static initialization for global mutexes:
static_mutex_t g_config_lock = STATIC_MUTEX_INIT;
void update_config(const char* key, const char* value) {
static_mutex_lock(&g_config_lock);
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;
const char* read_data(shared_resource_t* resource) {
rwlock_rdlock(&resource->lock);
const char* result = resource->data;
rwlock_rdunlock(&resource->lock);
return result;
}
void write_data(shared_resource_t* resource, const char* new_data) {
rwlock_wrlock(&resource->lock);
free(resource->data);
resource->data = strdup(new_data);
rwlock_wrunlock(&resource->lock);
}
Real-world example from ascii-chat:
static client_t** g_clients = NULL;
static size_t g_client_count = 0;
void broadcast_frame(const frame_t* frame) {
for (size_t i = 0; i < g_client_count; i++) {
send_frame_to_client(g_clients[i], frame);
}
}
g_clients = realloc(g_clients, (g_client_count + 1) * sizeof(client_t*));
g_clients[g_client_count++] = client;
}
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;
void wait_for_ready(sync_t* sync) {
mutex_lock(&sync->lock);
while (!sync->ready) {
cond_wait(&sync->cond, &sync->lock);
}
mutex_unlock(&sync->lock);
}
void set_ready(sync_t* sync) {
mutex_lock(&sync->lock);
sync->ready = true;
cond_signal(&sync->cond);
mutex_unlock(&sync->lock);
}
Timeout waiting:
mutex_lock(&sync->lock);
while (!sync->ready) {
int result = cond_timedwait(&sync->cond, &sync->lock, 5000);
if (result == ETIMEDOUT) {
log_warn("Timeout waiting for condition");
break;
}
}
mutex_unlock(&sync->lock);
Broadcasting to multiple waiters:
mutex_lock(&sync->lock);
sync->ready = true;
cond_broadcast(&sync->cond);
mutex_unlock(&sync->lock);
Static initialization:
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_mutex_unlock(&g_shutdown_lock);
}
void trigger_shutdown() {
static_mutex_lock(&g_shutdown_lock);
g_shutdown = true;
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"
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;
}
socket_set_reuseaddr(server_sock, true);
socket_set_nodelay(server_sock, true);
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);
socket_t client_sock = socket_accept(server_sock, NULL, NULL);
if (socket_is_valid(client_sock)) {
socket_close(client_sock);
}
socket_close(server_sock);
platform_cleanup();
return 0;
}
Automatic socket optimization:
When you accept a connection, the platform layer automatically optimizes it:
socket_t client_sock = socket_accept(server_sock, NULL, NULL);
You can override these settings if needed:
socket_set_nodelay(client_sock, false);
socket_setsockopt(client_sock, SOL_SOCKET, SO_RCVTIMEO, ...);
Non-blocking I/O:
socket_set_nonblocking(client_sock, true);
ssize_t sent = socket_send(client_sock, data, size, 0);
if (sent < 0) {
if (socket_get_last_error() == EWOULDBLOCK) {
}
}
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);
if (ready > 0) {
if (fds[0].revents & POLLIN) {
}
if (fds[1].revents & POLLIN) {
}
}
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"
terminal_size_t size;
printf("Terminal: %dx%d\n", size.cols, size.rows);
}
if (terminal_supports_color()) {
printf("Terminal supports color!\n");
}
if (terminal_supports_unicode()) {
printf("Terminal supports Unicode!\n");
}
platform_cleanup();
return 0;
}
Raw mode for interactive applications:
terminal_set_raw_mode(true);
terminal_set_echo(false);
char c;
read(STDIN_FILENO, &c, 1);
printf("You pressed: %c\n", c);
terminal_set_raw_mode(false);
terminal_set_echo(true);
Cursor control:
terminal_hide_cursor(true);
terminal_move_cursor(10, 20);
terminal_save_cursor();
terminal_move_cursor(1, 1);
printf("Hello from top-left!");
terminal_restore_cursor();
terminal_hide_cursor(false);
Screen manipulation:
terminal_clear_screen();
terminal_set_title("ascii-chat v1.0");
terminal_ring_bell();
terminal_set_scroll_region(5, 20);
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:
Platform sleep precision:
- Linux/macOS: Microsecond precision with nanosleep()
- Windows: ~15ms minimum resolution (system timer granularity)
Process information:
log_info("Process ID: %d", pid);
const char* user = platform_get_username();
log_info("Running as: %s", user);
pid_t platform_get_pid(void)
Environment variables:
if (home) {
printf("Home directory: %s\n", home);
}
int platform_setenv(const char *name, const char *value)
const char * platform_getenv(const char *name)
TTY detection:
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)
Signal handling:
void handle_sigint(int sig) {
log_info("Caught SIGINT, shutting down...");
g_shutdown = true;
}
platform_signal(SIGINT, handle_sigint);
Crash handlers (automatic backtrace on crash):
platform_cleanup();
return 0;
}
Manual backtrace (for debugging):
void* buffer[64];
for (int i = 0; i < count; i++) {
printf("%s\n", symbols[i]);
}
platform_backtrace_symbols_free(symbols);
int platform_backtrace(void **buffer, int size)
void platform_print_backtrace(int skip_frames)
char ** platform_backtrace_symbols(void *const *buffer, int size)
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:
char buffer[64];
platform_snprintf(buffer, sizeof(buffer), "Value: %d", 42);
Safe string copy:
Safe string concatenation:
char buffer[64] = "Hello";
Case-insensitive comparison:
printf("Strings are equal (case-insensitive)\n");
}
printf("First 3 characters match\n");
}
String duplication:
printf("%s\n", copy);
free(copy);
char* partial = platform_strndup("Hello, world!", 5);
printf("%s\n", partial);
free(partial);
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:
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];
if (bytes_read < 0) {
log_error("Read failed: %s", strerror(errno));
}
const char* data = "Hello, file!";
log_error("Write failed: %s", strerror(errno));
}
_Atomic uint64_t bytes_written
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:
log_error("Close failed: %s", strerror(errno));
}
int platform_close(int fd)
Force sync 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):
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) {
init();
}
pthread_mutex_lock(&g_mutex);
}
Platform abstraction approach (correct):
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
void use_mutex() {
static_mutex_lock(&g_mutex);
static_mutex_unlock(&g_mutex);
}
How It Works
The magic happens at different times on different platforms:
POSIX (Linux/macOS):
#define STATIC_MUTEX_INIT { .mutex = PTHREAD_MUTEX_INITIALIZER, .initialized = true }
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
Windows:
#define STATIC_MUTEX_INIT { .cs = {0}, .initialized = false }
int static_mutex_lock(static_mutex_t* mutex) {
if (!mutex->initialized) {
if (InterlockedCompareExchange(&mutex->initialized, 1, 0) == 0) {
InitializeCriticalSection(&mutex->cs);
mutex->initialized = 1;
} else {
while (!mutex->initialized) {
Sleep(0);
}
}
}
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);
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);
}
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():
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;
return NULL;
}
asciichat_thread_t thread;
platform_cleanup();
return 0;
}
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:
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
platform_cleanup();
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:
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):
typedef struct {
CRITICAL_SECTION cs;
} mutex_t;
InitializeCriticalSectionAndSpinCount(&mutex->cs, 4000);
mutex->initialized = true;
return 0;
}
Spin count of 4000 means the thread will spin 4000 times before sleeping, which improves performance for short-held locks.
Read-Write Locks (SRWLOCK):
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:
- Allocate thread wrapper structure
- Initialize symbol handler (first thread only)
- Create Windows thread with wrapper function
- Wrapper catches exceptions and generates backtraces
- Call user's thread function
- 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 {
ensure_symbol_handler_initialized();
__try {
void* result = wrapper_arg->start_routine(wrapper_arg->arg);
return (DWORD)(uintptr_t)result;
}
__except(exception_filter(GetExceptionInformation())) {
}
}
__finally {
}
}
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:
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;
frame.AddrStack.Offset = context.Rsp;
frame.AddrFrame.Offset = context.Rbp;
while (StackWalk64(IMAGE_FILE_MACHINE_AMD64, process, thread,
&frame, &context, NULL, SymFunctionTableAccess64,
SymGetModuleBase64, NULL)) {
}
Fallback methods:
- Manual stack walking using frame pointers
- Symbol resolution using addr2line.exe
- 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:
return pthread_create(&thread->thread, NULL, func, arg);
}
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:
platform_signal(SIGINT, sigint_handler);
platform_signal(SIGTERM, sigterm_handler);
platform_signal(SIGWINCH, resize_handler);
platform_signal(SIGUSR1, user_handler);
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;
sigaction(sig, &new_action, &old_action);
return old_action.sa_handler;
}
POSIX Backtrace
POSIX platforms use the standard execinfo.h backtrace API:
return backtrace(buffer, 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__
return pthread_join(thread->thread, retval);
#else
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:
typedef struct { pthread_t thread; } thread_t;
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 {
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);
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;
}
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");
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;
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;
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;
}
queue_t queue;
queue_init(&queue);
asciichat_thread_t prod_thread, cons_thread;
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);
}
fprintf(stderr, "This program must be run in a terminal\n");
return 1;
}
terminal_set_raw_mode(true);
terminal_set_echo(false);
terminal_hide_cursor(true);
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;
}
}
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.
platform_cleanup();
return 0;
}
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.
#ifdef _WIN32
HANDLE thread;
CreateThread(...);
#else
pthread_t thread;
pthread_create(...);
#endif
asciichat_thread_t thread;
If you find yourself writing platform-specific code:
- Check if the platform layer already provides what you need
- If not, add it to the platform layer (don't scatter ifdefs!)
- Keep all platform differences isolated in
lib/platform/
Use Platform Types
Rule: Use platform abstraction types, not native types.
pthread_t thread;
CRITICAL_SECTION cs;
int sockfd;
asciichat_thread_t thread;
mutex_t mutex;
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.
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_bind(sock, &addr, sizeof(addr));
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.
static mutex_t g_mutex;
static bool g_mutex_initialized = false;
void init() {
if (!g_mutex_initialized) {
g_mutex_initialized = true;
}
}
void use_mutex() {
if (!g_mutex_initialized) init();
mutex_lock(&g_mutex);
}
static_mutex_t g_mutex = STATIC_MUTEX_INIT;
void use_mutex() {
static_mutex_lock(&g_mutex);
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.
asciichat_thread_t thread;
asciichat_thread_t thread;
bool thread_created = false;
thread_created = true;
} else {
log_error("Thread creation failed");
}
if (thread_created) {
}
Or use a helper function:
if (asciichat_thread_is_initialized(&thread)) {
}
Socket Validation
Rule: Use socket_is_valid() to check socket validity, not comparisons.
if (sock < 0) { ... }
if (sock == NULL) { ... }
if (sock >= 0) { ... }
if (!socket_is_valid(sock)) {
log_error("Invalid socket");
}
if (socket_is_valid(sock)) {
}
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:
#include <pthread.h>
pthread_t thread;
pthread_create(&thread, NULL, worker, arg);
pthread_join(thread, NULL);
#include "platform/abstraction.h"
asciichat_thread_t thread;
From Windows threads:
#include <windows.h>
HANDLE thread = CreateThread(NULL, 0, worker, arg, 0, NULL);
WaitForSingleObject(thread, INFINITE);
CloseHandle(thread);
#include "platform/abstraction.h"
asciichat_thread_t thread;
Migrating Mutexes
From POSIX mutexes:
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
mutex_t mutex;
mutex_lock(&mutex);
mutex_unlock(&mutex);
From Windows critical sections:
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
mutex_t mutex;
mutex_lock(&mutex);
mutex_unlock(&mutex);
Migrating Sockets
From POSIX sockets:
#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);
#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:
#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();
#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);
platform_cleanup();
Migrating Terminal I/O
From termios (POSIX):
#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);
#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:
#include <signal.h>
void handler(int sig) { }
signal(SIGINT, handler);
#include "platform/abstraction.h"
void handler(int sig) { }
platform_signal(SIGINT, handler);
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:
socket_t sock = socket_create(AF_INET, SOCK_STREAM, 0);
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;
}
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:
typedef struct {
asciichat_thread_t thread;
bool completed;
mutex_t lock;
} watchdog_t;
void* watchdog_func(void* arg) {
watchdog_t* wd = (watchdog_t*)arg;
mutex_lock(&wd->lock);
if (!wd->completed) {
log_warn("Thread did not complete in time!");
}
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);
socket_set_nodelay(client, false);
socket_setsockopt(client, SOL_SOCKET, SO_RCVTIMEO, ...);
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);
timeEndPeriod(1);
#else
#endif
Contributing
Want to add new platform abstractions or improve existing ones?
Adding New Platform APIs
When adding a new platform abstraction:
- 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
- Implement for POSIX (
lib/platform/posix/foo.c): #include "platform/foo.h"
#include <posix_foo.h>
struct foo_s {
posix_foo_t native_foo;
};
int foo_init(foo_t* foo) {
return posix_foo_init(&foo->native_foo);
}
- 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;
}
- Add to abstraction.h:
#include "platform/foo.h"
- Document thoroughly with Doxygen comments
- 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) {
}
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