P04: Cross-Platform Syscall Abstraction Library
P04: Cross-Platform Syscall Abstraction Library
Build a library that wraps platform-specific syscalls into a unified API like libuv or Rustโs std
| Attribute | Value |
|---|---|
| Main Language | C |
| Difficulty | Level 3: Advanced |
| Coolness Level | Level 2: Practical but Foundational |
| Knowledge Area | Systems Programming, Portability |
| Key Tools | POSIX APIs, Win32 API, CMake |
| Main Book | โAdvanced Programming in the UNIX Environmentโ - Stevens & Rago |
Learning Objectives
By completing this project, you will:
- Master platform abstractions โ Understand the fundamental differences between POSIX and Windows
- Handle ABI details โ Learn about struct layouts, calling conventions, and binary compatibility
- Design stable APIs โ Create interfaces that hide platform complexity without sacrificing performance
- Avoid undefined behavior โ Navigate the minefield of type punning, alignment, and strict aliasing
- Build portable software โ Write code that compiles and runs identically across operating systems
- Use CI/CD effectively โ Test on multiple platforms automatically
The Core Question
โWhy canโt I just write code that works everywhere? What makes POSIX and Windows fundamentally different?โ
Most developers treat cross-platform compatibility as annoying #ifdef boilerplate. But understanding why platforms differ reveals deep truths about operating system design:
- What is a file descriptor vs a HANDLE? Both represent โopen files,โ but theyโre completely different concepts
- Why doesnโt Windows have fork()? Itโs not a missing featureโitโs architecturally incompatible
- What makes a good abstraction? How do you hide differences without killing performance?
After this project, youโll understand that portable code isnโt about avoiding platform-specific APIsโitโs about building the right abstraction layer.
Deep Theoretical Foundation
1. POSIX File Descriptors vs Windows HANDLEs
File Descriptors (POSIX)
A file descriptor is just an integerโan index into a per-process table:
Process File Descriptor Table:
โโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FD โ Points to (in kernel) โ
โโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 0 โ stdin (terminal) โ
โ 1 โ stdout (terminal) โ
โ 2 โ stderr (terminal) โ
โ 3 โ /home/user/data.txt โ
โ 4 โ socket to 192.168.1.1:80 โ
โโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int fd = open("/path/to/file", O_RDONLY); // Returns small integer (e.g., 3)
read(fd, buffer, size); // fd is just an int
close(fd);
HANDLEs (Windows)
A HANDLE is an opaque pointer to a kernel object:
HANDLE h = CreateFileA("C:\\path\\file.txt",
GENERIC_READ,
0, NULL, OPEN_EXISTING, 0, NULL);
// h is a pointer-sized value (often looks like 0x0000000000000108)
ReadFile(h, buffer, size, &bytes_read, NULL);
CloseHandle(h);
Key Differences
| Aspect | File Descriptor | HANDLE |
|---|---|---|
| Type | int (small integer) |
void* (pointer-sized) |
| Validity | fd >= 0 means valid |
Compare to INVALID_HANDLE_VALUE |
| Inheritance | Automatic to child processes | Explicit via SetHandleInformation |
| I/O model | read()/write() |
ReadFile()/WriteFile() with overlapped |
2. Process Creation Models
POSIX: fork() + exec()
pid_t pid = fork();
if (pid == 0) {
// Child process - exact copy of parent
exec("/bin/ls", "ls", "-la", NULL); // Replace with new program
} else {
// Parent process
waitpid(pid, &status, 0);
}
fork() creates an exact duplicate of the parent process:
- Same memory contents (copy-on-write)
- Same open files
- Same state
Windows: CreateProcess()
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcessA(
"C:\\Windows\\System32\\cmd.exe", // Program
"/c dir", // Arguments
NULL, NULL, // Security attributes
FALSE, // Inherit handles
0, // Flags
NULL, // Environment
NULL, // Working directory
&si, &pi
);
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
Why Windows Has No fork()
-
DLL base addresses: Windows DLLs can be loaded at different addresses in different processes.
fork()would need to handle this. -
Thread state: Windows processes often have multiple threads. Duplicating mid-execution is undefined.
-
Kernel object handles: Handles have process-specific meaning. Mass duplication is complex.
-
Historical design: Windows was designed differently from Unix from the start.
3. Error Handling Conventions
POSIX
int fd = open("/path/to/file", O_RDONLY);
if (fd == -1) {
// Error! Check errno (thread-local)
printf("Error: %s\n", strerror(errno));
// errno might be ENOENT, EACCES, EMFILE, etc.
}
Windows
HANDLE h = CreateFileA(...);
if (h == INVALID_HANDLE_VALUE) {
// Error! Call GetLastError() (thread-local)
DWORD err = GetLastError();
printf("Error code: %lu\n", err);
// Error might be ERROR_FILE_NOT_FOUND, ERROR_ACCESS_DENIED, etc.
}
Error Code Mapping
| POSIX errno | Windows Error | Meaning |
|---|---|---|
ENOENT |
ERROR_FILE_NOT_FOUND |
File not found |
EACCES |
ERROR_ACCESS_DENIED |
Permission denied |
EEXIST |
ERROR_FILE_EXISTS |
File exists |
EMFILE |
ERROR_TOO_MANY_OPEN_FILES |
Too many open files |
4. Struct Layout and ABI
The Problem
struct stat {
dev_t st_dev;
ino_t st_ino;
mode_t st_mode;
// ...
};
This struct has different sizes and layouts on:
- Linux (glibc)
- Linux (musl)
- macOS
- FreeBSD
- 32-bit vs 64-bit
Why?
- Type sizes differ:
ino_tmight be 32 or 64 bits - Padding differs: Compilers insert different padding for alignment
- Field order might differ: Some add fields, changing offsets
Implications
You cannot:
- Share binary struct data between platforms
- Use
sizeof(struct stat)in portable code - Cast pointers between platform-specific structs
5. Calling Conventions
Different platforms use different rules for passing function arguments:
| Convention | Used By | Args in Registers | Stack Cleanup |
|---|---|---|---|
| cdecl | Most C code | None (x86) | Caller |
| stdcall | Win32 API | None (x86) | Callee |
| fastcall | Some Win32 | First 2 in regs | Callee |
| System V AMD64 | Linux/macOS x64 | rdi, rsi, rdx, rcx, r8, r9 | Caller |
| Microsoft x64 | Windows x64 | rcx, rdx, r8, r9 | Caller |
Why It Matters
When calling across boundaries (e.g., from your library into the OS), you must use the correct convention:
// Windows-specific
BOOL __stdcall MyCallback(LPVOID context);
// POSIX
void* my_thread_func(void* arg);
6. Thread-Local Storage
POSIX
pthread_key_t key;
pthread_key_create(&key, destructor);
pthread_setspecific(key, value);
void* value = pthread_getspecific(key);
Or with GCC extension:
__thread int my_tls_var;
Windows
DWORD tls_index = TlsAlloc();
TlsSetValue(tls_index, value);
void* value = TlsGetValue(tls_index);
TlsFree(tls_index);
Or with MSVC:
__declspec(thread) int my_tls_var;
7. Symbol Visibility and Linking
Exporting Symbols
To make functions available from a shared library:
// POSIX (GCC/Clang)
__attribute__((visibility("default")))
void my_public_function(void);
// Windows
__declspec(dllexport)
void my_public_function(void);
// Or when importing:
__declspec(dllimport)
void their_public_function(void);
Common Macro Pattern
#ifdef _WIN32
#ifdef MYLIB_BUILDING
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API __attribute__((visibility("default")))
#endif
MYLIB_API void my_function(void);
Project Specification
What Youโll Build
A cross-platform abstraction library providing:
// xplat.h - Unified API
// File operations
xplat_file_t* xplat_open(const char* path, int flags);
ssize_t xplat_read(xplat_file_t* file, void* buf, size_t count);
ssize_t xplat_write(xplat_file_t* file, const void* buf, size_t count);
int xplat_close(xplat_file_t* file);
// Directory operations
xplat_dir_t* xplat_opendir(const char* path);
int xplat_readdir(xplat_dir_t* dir, xplat_dirent_t* entry);
int xplat_closedir(xplat_dir_t* dir);
// Process spawning
xplat_process_t* xplat_spawn(const char* cmd, char** args, char** env);
int xplat_wait(xplat_process_t* proc, int* exit_code);
int xplat_kill(xplat_process_t* proc);
// Threading
xplat_thread_t* xplat_thread_create(xplat_thread_fn func, void* arg);
int xplat_thread_join(xplat_thread_t* thread, void** result);
// Error handling
int xplat_errno(void);
const char* xplat_strerror(int err);
Deliverables
- Public header:
include/xplat.hโ Platform-agnostic API - POSIX implementation:
src/xplat_posix.c - Windows implementation:
src/xplat_win32.c - Build system: CMake supporting all platforms
- Test suite: 40+ tests covering all functionality
- CI/CD: GitHub Actions building on Linux, macOS, Windows
- Documentation: API reference and usage guide
Success Criteria
# Same code works everywhere
$ cat demo.c
#include <xplat.h>
int main() {
xplat_file_t* f = xplat_open("test.txt", XPLAT_O_RDONLY);
// ...
}
# Linux
$ gcc demo.c -lxplat -o demo && ./demo
OK
# macOS
$ clang demo.c -lxplat -o demo && ./demo
OK
# Windows
C:\> cl demo.c xplat.lib /Fe:demo.exe && demo.exe
OK
# CI passes on all platforms
$ gh run view
โ ubuntu-latest
โ macos-latest
โ windows-latest
Solution Architecture
Component Overview
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Application Code โ
โ xplat_open(), xplat_read(), xplat_spawn(), ... โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Public API (xplat.h) โ
โ Opaque types, flag definitions, error codes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ POSIX Implementation โ โ Windows Implementation โ
โ (xplat_posix.c) โ โ (xplat_win32.c) โ
โ โ โ โ
โ open() โ xplat_open() โ โ CreateFile โ xplat_open() โ
โ read() โ xplat_read() โ โ ReadFile โ xplat_read() โ
โ fork/exec โ xplat_spawn() โ โ CreateProcess โ xplat_spawnโ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Linux/macOS/BSD โ โ Windows โ
โ Kernel โ โ Kernel โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Data Structures
Public Opaque Types
// xplat.h
typedef struct xplat_file xplat_file_t;
typedef struct xplat_dir xplat_dir_t;
typedef struct xplat_process xplat_process_t;
typedef struct xplat_thread xplat_thread_t;
Internal Structures (POSIX)
// xplat_posix.c
struct xplat_file {
int fd;
int flags;
};
struct xplat_process {
pid_t pid;
int status;
bool waited;
};
Internal Structures (Windows)
// xplat_win32.c
struct xplat_file {
HANDLE handle;
DWORD access;
};
struct xplat_process {
HANDLE process;
HANDLE thread;
DWORD exit_code;
};
Flag Translation Layer
// xplat.h - Platform-independent flags
#define XPLAT_O_RDONLY 0x01
#define XPLAT_O_WRONLY 0x02
#define XPLAT_O_RDWR 0x04
#define XPLAT_O_CREAT 0x08
#define XPLAT_O_TRUNC 0x10
#define XPLAT_O_APPEND 0x20
// xplat_posix.c
static int translate_flags_posix(int xplat_flags) {
int flags = 0;
if (xplat_flags & XPLAT_O_RDONLY) flags |= O_RDONLY;
if (xplat_flags & XPLAT_O_WRONLY) flags |= O_WRONLY;
if (xplat_flags & XPLAT_O_RDWR) flags |= O_RDWR;
if (xplat_flags & XPLAT_O_CREAT) flags |= O_CREAT;
if (xplat_flags & XPLAT_O_TRUNC) flags |= O_TRUNC;
if (xplat_flags & XPLAT_O_APPEND) flags |= O_APPEND;
return flags;
}
// xplat_win32.c
static DWORD translate_access_win32(int xplat_flags) {
DWORD access = 0;
if (xplat_flags & XPLAT_O_RDONLY) access |= GENERIC_READ;
if (xplat_flags & XPLAT_O_WRONLY) access |= GENERIC_WRITE;
if (xplat_flags & XPLAT_O_RDWR) access |= GENERIC_READ | GENERIC_WRITE;
return access;
}
Error Code Mapping
// Unified error codes
typedef enum {
XPLAT_OK = 0,
XPLAT_ERR_NOT_FOUND,
XPLAT_ERR_PERMISSION,
XPLAT_ERR_EXISTS,
XPLAT_ERR_TOO_MANY_FILES,
XPLAT_ERR_INVALID,
XPLAT_ERR_IO,
XPLAT_ERR_UNKNOWN,
} xplat_error_t;
// POSIX translation
static xplat_error_t errno_to_xplat(int err) {
switch (err) {
case ENOENT: return XPLAT_ERR_NOT_FOUND;
case EACCES: return XPLAT_ERR_PERMISSION;
case EEXIST: return XPLAT_ERR_EXISTS;
case EMFILE: return XPLAT_ERR_TOO_MANY_FILES;
case EINVAL: return XPLAT_ERR_INVALID;
case EIO: return XPLAT_ERR_IO;
default: return XPLAT_ERR_UNKNOWN;
}
}
// Windows translation
static xplat_error_t win_error_to_xplat(DWORD err) {
switch (err) {
case ERROR_FILE_NOT_FOUND: return XPLAT_ERR_NOT_FOUND;
case ERROR_ACCESS_DENIED: return XPLAT_ERR_PERMISSION;
case ERROR_FILE_EXISTS: return XPLAT_ERR_EXISTS;
// ...
}
}
Phased Implementation Guide
Phase 1: Project Setup (Day 1)
Goal: Build system that compiles on all platforms.
Steps:
- Create CMakeLists.txt with platform detection
- Set up CI with GitHub Actions (matrix: ubuntu, macos, windows)
- Create header with platform detection macros
- Verify empty library builds everywhere
Test: cmake --build . succeeds on all platforms.
Phase 2: File Operations (Days 2-3)
Goal: xplat_open, xplat_read, xplat_write, xplat_close.
Steps:
- Define opaque
xplat_file_t - Implement POSIX version with open/read/write/close
- Implement Windows version with CreateFile/ReadFile/WriteFile/CloseHandle
- Add flag translation
- Add error code translation
Test: Can open, read, write, close files on all platforms.
Phase 3: Directory Operations (Days 3-4)
Goal: xplat_opendir, xplat_readdir, xplat_closedir.
Steps:
- Define opaque
xplat_dir_tandxplat_dirent_t - Implement POSIX with opendir/readdir/closedir
- Implement Windows with FindFirstFile/FindNextFile/FindClose
- Handle the different iteration patterns
Test: Can list directory contents on all platforms.
Phase 4: Process Spawning (Days 4-5)
Goal: xplat_spawn, xplat_wait, xplat_kill.
Steps:
- Define opaque
xplat_process_t - Implement POSIX with fork/exec/waitpid
- Implement Windows with CreateProcess/WaitForSingleObject/TerminateProcess
- Handle argument passing differences
- Handle environment variable passing
Test: Can spawn process, wait for exit, get exit code.
Phase 5: Threading (Days 5-6)
Goal: xplat_thread_create, xplat_thread_join, xplat_mutex_*.
Steps:
- Define opaque types
- Implement POSIX with pthreads
- Implement Windows with CreateThread/_beginthreadex
- Handle thread return values
- Implement mutexes and condition variables
Test: Can create threads, synchronize with mutexes.
Phase 6: Polish and Documentation (Days 7-10)
Goal: Production-ready library.
Steps:
- Comprehensive test suite
- Error message improvements
- API documentation
- Usage examples
- Performance benchmarks
Testing Strategy
Unit Tests
// test_file.c
void test_open_read_close(void) {
xplat_file_t* f = xplat_open("testdata/sample.txt", XPLAT_O_RDONLY);
assert(f != NULL);
char buf[256];
ssize_t n = xplat_read(f, buf, sizeof(buf));
assert(n > 0);
int err = xplat_close(f);
assert(err == 0);
}
void test_write_create(void) {
xplat_file_t* f = xplat_open("test_out.txt",
XPLAT_O_WRONLY | XPLAT_O_CREAT | XPLAT_O_TRUNC);
assert(f != NULL);
const char* data = "Hello, World!";
ssize_t n = xplat_write(f, data, strlen(data));
assert(n == strlen(data));
xplat_close(f);
}
void test_error_handling(void) {
xplat_file_t* f = xplat_open("/nonexistent/path", XPLAT_O_RDONLY);
assert(f == NULL);
assert(xplat_errno() == XPLAT_ERR_NOT_FOUND);
}
Cross-Platform Tests
// test_process.c
void test_spawn_and_wait(void) {
// Platform-specific command that should exist everywhere
#ifdef _WIN32
const char* cmd = "cmd.exe";
char* args[] = {"/c", "echo", "hello", NULL};
#else
const char* cmd = "/bin/echo";
char* args[] = {"echo", "hello", NULL};
#endif
xplat_process_t* proc = xplat_spawn(cmd, args, NULL);
assert(proc != NULL);
int exit_code;
int err = xplat_wait(proc, &exit_code);
assert(err == 0);
assert(exit_code == 0);
}
CI Matrix Testing
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
compiler: [gcc, clang, msvc]
exclude:
- os: windows-latest
compiler: gcc
- os: ubuntu-latest
compiler: msvc
- os: macos-latest
compiler: msvc
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Configure
run: cmake -B build
- name: Build
run: cmake --build build
- name: Test
run: ctest --test-dir build --output-on-failure
Common Pitfalls and Debugging
Pitfall 1: Path Separators
Symptom: File operations fail on Windows.
Cause: Using / instead of \.
Fix:
void normalize_path(char* path) {
#ifdef _WIN32
for (char* p = path; *p; p++) {
if (*p == '/') *p = '\\';
}
#endif
}
// Or better: accept both and handle internally
Pitfall 2: Text vs Binary Mode
Symptom: Files have extra \r characters on Windows.
Cause: Windows translates \n to \r\n in text mode.
Fix:
// Always use binary mode for raw byte operations
#ifdef _WIN32
_setmode(_fileno(stdin), _O_BINARY);
#endif
// In CreateFile:
// Don't add FILE_FLAG_SEQUENTIAL_SCAN unless needed
Pitfall 3: sizeof(long) Differences
Symptom: Data corruption when serializing.
Cause: long is 4 bytes on Windows (LLP64) but 8 bytes on Linux (LP64).
Fix:
// Use fixed-size types
#include <stdint.h>
int32_t value; // Always 4 bytes
int64_t large; // Always 8 bytes
// NOT: long value; // Platform-dependent!
Pitfall 4: Thread Function Signatures
Symptom: Crashes or undefined behavior with threads.
Cause: Different calling conventions expected.
Fix:
// POSIX thread function
void* posix_thread_func(void* arg);
// Windows thread function
DWORD WINAPI win_thread_func(LPVOID arg);
// Wrapper approach
typedef void (*xplat_thread_fn)(void* arg);
// Internally convert:
#ifdef _WIN32
static DWORD WINAPI thread_wrapper(LPVOID arg) {
xplat_thread_internal_t* t = arg;
t->func(t->arg);
return 0;
}
#endif
Pitfall 5: Missing NULL Terminators
Symptom: Crashes when spawning processes on some platforms.
Cause: Windows doesnโt require NULL-terminated arg arrays like POSIX exec.
Fix:
// Always NULL-terminate
char* args[] = {"program", "arg1", "arg2", NULL}; // Correct
// Windows needs command line as string anyway
// POSIX needs NULL terminator for execv
Extensions and Challenges
Extension 1: Memory-Mapped Files
xplat_mmap_t* xplat_mmap(xplat_file_t* file, size_t length, int prot, int flags);
int xplat_munmap(xplat_mmap_t* mapping);
Extension 2: Sockets
xplat_socket_t* xplat_socket(int domain, int type, int protocol);
int xplat_connect(xplat_socket_t* sock, const char* host, int port);
ssize_t xplat_send(xplat_socket_t* sock, const void* buf, size_t len);
ssize_t xplat_recv(xplat_socket_t* sock, void* buf, size_t len);
Extension 3: Dynamic Libraries
xplat_lib_t* xplat_dlopen(const char* path);
void* xplat_dlsym(xplat_lib_t* lib, const char* symbol);
int xplat_dlclose(xplat_lib_t* lib);
Extension 4: Filesystem Monitoring
xplat_watch_t* xplat_watch(const char* path);
int xplat_watch_poll(xplat_watch_t* w, xplat_event_t* events, int max);
// Uses inotify (Linux), FSEvents (macOS), ReadDirectoryChangesW (Windows)
Real-World Connections
Libraries Using These Patterns
| Library | Purpose | Key Insight |
|---|---|---|
| libuv | Powers Node.js | Event loop + fs + process abstraction |
| SDL | Game development | Window, input, audio abstraction |
| GLFW | OpenGL windowing | Minimal window abstraction |
| Rust std | Standard library | Comprehensive sys abstraction |
| Go runtime | Goโs syscall package | Platform-agnostic interface |
Study libuvโs Source
git clone https://github.com/libuv/libuv.git
cd libuv
# Platform-specific implementations
ls src/unix/ # Linux, macOS, BSD
ls src/win/ # Windows
# Public headers
cat include/uv.h
# See how they handle file operations
grep -r "uv_fs_open" src/
Resources
Essential Reading
- โAdvanced Programming in the UNIX Environmentโ by Stevens & Rago โ POSIX fundamentals
- โWindows System Programmingโ by Johnson Hart โ Win32 API
- โWindows Via C/C++โ by Jeffrey Richter โ Deep Windows internals
Online Resources
Code to Study
- libuv โ Best-in-class cross-platform I/O
- Rust std::sys โ Rustโs platform abstractions
- SDL2 โ Mature cross-platform library
Self-Assessment Checklist
Before considering this project complete, verify:
Understanding
- I can explain the difference between file descriptors and HANDLEs
- I can explain why Windows doesnโt have fork()
- I can describe three things that differ in struct layout across platforms
- I can explain what a calling convention is
- I can describe how to export symbols from a shared library on both platforms
Implementation
- My library compiles on Linux, macOS, and Windows
- File operations work identically across platforms
- Process spawning works with proper argument passing
- Error codes are unified and meaningful
- CI builds and tests on all platforms
Verification
- Iโve tested with at least 40 test cases
- Iโve tested edge cases (empty files, long paths, Unicode)
- Someone else can use my library with only the public header
- Documentation explains any platform-specific limitations
Cross-platform development forces you to truly understand what operating systems do. After this project, youโll know why โportable Cโ is harder than it sounds.