Project 4: Cross-Platform Syscall Abstraction Library

Build a portable C library that hides POSIX vs Windows differences behind a stable ABI.

Quick Reference

Attribute Value
Difficulty Level 3: Advanced
Time Estimate 2 to 3 weeks
Main Programming Language C (C17)
Alternative Programming Languages C++, Rust
Coolness Level Level 3: Serious Systems Builder
Business Potential 2 - Infrastructure Core
Prerequisites C structs, basic file I/O, build systems
Key Topics ABI stability, opaque types, error mapping, portability

1. Learning Objectives

By completing this project, you will:

  1. Design a stable C ABI that can survive refactors.
  2. Wrap POSIX and Windows system calls behind a unified API.
  3. Normalize error handling into a portable error model.
  4. Build a portability layer with clear separation of platform code.
  5. Ship a demo that behaves identically on Linux and Windows.

2. All Theory Needed (Per-Concept Breakdown)

Concept 1: ABI Stability and Opaque Types

Fundamentals

An ABI is the binary contract between compiled code and a library. If you change struct layout, calling conventions, or symbol names, you can break programs that were compiled against an earlier version. API stability is about source compatibility, but ABI stability is about binary compatibility. In a C library, the most common ABI pitfalls are exposing struct layouts in public headers, changing enum sizes, or adding fields to public structs. The standard solution is to use opaque types: the header declares a struct but hides its fields, and the implementation defines the actual layout. This makes it safe to change internals without breaking binary compatibility. ABI stability also includes versioning, symbol visibility, and careful control of inline functions.

Deep Dive into the concept

ABI stability is often underestimated because it is invisible when you build everything from source. The breakage only appears when a program links against a shared library and the library changes in a way that the program does not expect. In C, the compiler assumes struct layout at compile time. If a struct grows or changes alignment, the program will still use the old layout and read the wrong fields. This is why public structs should be opaque. Instead of exposing fields, you expose accessor functions. This costs a function call but preserves binary stability.

Another ABI hazard is calling conventions. On most platforms the default calling convention is stable, but if you compile with different flags or use different compilers, differences can appear. You should keep your public API to plain C types and avoid compiler-specific extensions. This also means avoiding passing structs by value across the ABI boundary; instead, use pointers or size_t-based handles.

Symbol visibility matters too. If you compile a shared library with all symbols exported, you risk accidental ABI exposure. A private helper function could become part of the public ABI without you realizing it. You should use visibility annotations or linker scripts to export only the intended API. This is a common practice in system libraries such as libc. Another best practice is to include a version macro and a function that returns a version string so that clients can check compatibility at runtime.

Inline functions are a subtle ABI trap. If you provide inline functions in headers, client code will inline the old implementation and will not pick up changes unless recompiled. That can break semantics even if the ABI is technically stable. For this project, keep inline usage minimal and prefer normal functions.

Opaque types also allow you to change memory allocation strategies. If you expose struct fields, you must maintain the same size and alignment forever. But if the struct is opaque, you can add fields, change alignment, or reorganize memory without breaking clients. This is particularly useful in a cross-platform library where you may need different fields on different OSes. The opaque type keeps the ABI consistent while the implementation adapts.

Finally, ABI stability requires documentation and discipline. You should define which parts of the API are stable, which are experimental, and how versioning works. A simple strategy is semantic versioning: bump major on ABI breaks. Even in a small project, this discipline helps you learn how real system libraries manage compatibility.

How This Fits on Projects

This concept drives the public header design in Section 3.2 and the component boundaries in Section 4.2. It is also relevant to plugin interfaces in P01-custom-memory-allocator.md and to stable APIs in P05-high-performance-string-search.md.

Definitions & Key Terms

  • ABI: Binary interface between compiled code and a library.
  • Opaque type: A struct type whose fields are hidden from users.
  • Symbol visibility: Which functions are exported from a shared library.
  • Calling convention: Rules for passing arguments and returning values.
  • Semantic versioning: Versioning scheme indicating compatibility.

Mental Model Diagram (ASCII)

Client code -> public header -> shared library -> private structs

How It Works (Step-by-Step)

  1. Public header declares typedef struct xplat_file xplat_file_t;.
  2. Library exposes functions that operate on xplat_file_t*.
  3. Actual struct definition lives in a private source file.
  4. Library exports only intended symbols.

Invariants:

  • Public headers never expose struct layouts.
  • Public API uses stable types only.

Failure modes:

  • Changing a public struct field breaks binaries.
  • Exposing helper symbols unintentionally expands ABI surface.

Minimal Concrete Example

// public header
typedef struct xplat_file xplat_file_t;

xplat_file_t* xplat_open(const char* path);
void xplat_close(xplat_file_t* f);

Common Misconceptions

  • “API stability is enough” -> ABI breaks can still crash binaries.
  • “Inline functions are free” -> They freeze behavior in client code.
  • “Struct changes are safe” -> They break compiled code that assumes layout.

Check-Your-Understanding Questions

  1. Why are opaque structs used in system libraries?
  2. How can inline functions cause ABI issues?
  3. Why should you limit exported symbols?

Check-Your-Understanding Answers

  1. They allow internal changes without breaking client binaries.
  2. Client code inlines old logic and will not pick up fixes.
  3. Exporting only intended APIs prevents accidental ABI commitments.

Real-World Applications

  • libc and libuv hide implementation details behind opaque handles.
  • Graphics APIs expose handles rather than struct layouts.

Where You’ll Apply It

References

  • “Computer Systems: A Programmer’s Perspective” - linking and ABI.
  • “C Interfaces and Implementations” - interface design.

Key Insights

Opaque types are the simplest and most effective tool for ABI stability.

Summary

ABI stability is about binary contracts, not source code. Opaque types, restricted symbol visibility, and disciplined headers are the core techniques.

Homework/Exercises to Practice the Concept

  1. Create a library with a public struct and observe ABI break when you add a field.
  2. Convert the struct to an opaque type and rebuild.
  3. Use a version macro and compare at runtime.

Solutions to the Homework/Exercises

  1. The client crashes or reads incorrect fields after the struct change.
  2. The opaque type hides changes and preserves ABI stability.
  3. The version macro lets the client detect mismatches safely.

Concept 2: POSIX vs Windows System Interfaces

Fundamentals

Cross-platform libraries must unify very different OS APIs. POSIX uses file descriptors (small integers) and calls like open, read, write, and close. Windows uses HANDLEs and functions like CreateFile, ReadFile, and CloseHandle. Error reporting differs: POSIX returns -1 and sets errno, while Windows returns a sentinel value and uses GetLastError. Even basic behavior differs: Windows and POSIX have different path conventions, file sharing semantics, and process models. A portable library must decide what functionality to expose and how to map semantics that do not align. The goal is not to expose every platform-specific feature, but to define a stable, predictable subset.

Deep Dive into the concept

The first step in cross-platform design is to identify the minimum viable API that can be implemented consistently. For example, file operations such as open, read, write, and close can be mapped on both platforms. However, advanced POSIX features like fork do not have direct equivalents on Windows. You must either omit them or emulate them with different semantics. This is a design choice you should document clearly.

File I/O differences are subtle. POSIX file descriptors are small integers that can be duplicated, polled, and inherited by child processes. Windows HANDLEs are opaque pointers that require different APIs for duplication and inheritance. POSIX allows read and write to be used for sockets and files uniformly; Windows uses different calls for sockets (recv, send) and files (ReadFile, WriteFile). You can choose to provide a single abstraction that hides these differences, but you must be aware of the limitations. For example, you may decide to provide only file I/O and a separate socket abstraction in a later version.

Time functions also differ. POSIX uses clock_gettime with timespec, while Windows uses QueryPerformanceCounter or GetSystemTimeAsFileTime. The resolution and epoch differ. A portability layer should provide a consistent monotonic time function in a platform-neutral format, such as nanoseconds since an arbitrary start. This avoids confusion and makes timer code portable.

Process creation is another major difference. POSIX has fork and exec, which separate process creation and program execution. Windows has CreateProcess which does both at once. This makes it hard to provide a single API that exactly matches both. The typical approach is to offer a simplified process spawn API that accepts a command line and environment, mapping to posix_spawn or CreateProcess. This yields consistent behavior but limited flexibility.

Finally, path handling and file metadata are a minefield. Windows uses backslashes and drive letters; POSIX uses forward slashes and a single root. Case sensitivity differs, and path length limits are different. For a simple library, you can accept POSIX-style paths on POSIX and Windows-style paths on Windows, but this reduces portability of the calling code. Alternatively, you can normalize paths inside the library, but that increases complexity. The best approach is to document the expected path format and provide helpers for canonicalization.

How This Fits on Projects

This concept defines the OS abstraction layer in Section 4.2 and the platform-specific implementations in Section 5.2. It also connects to non-blocking I/O in P03-mini-async-runtime.md and file I/O in P06-embedded-key-value-database.md.

Definitions & Key Terms

  • File descriptor: POSIX integer handle for I/O objects.
  • HANDLE: Windows opaque handle for kernel objects.
  • errno: POSIX thread-local error code.
  • GetLastError: Windows API for last error code.
  • Path normalization: Converting paths into a canonical format.

Mental Model Diagram (ASCII)

POSIX: open/read/write/close -> fd
Windows: CreateFile/ReadFile/WriteFile -> HANDLE
Library: xplat_file -> uniform API

How It Works (Step-by-Step)

  1. User calls xplat_open(path).
  2. On POSIX, call open and wrap fd.
  3. On Windows, call CreateFile and wrap HANDLE.
  4. Map errors to a common error type.

Invariants:

  • Public API behaves the same on both platforms.
  • Platform-specific code is isolated in one module.

Failure modes:

  • Subtle semantic differences (sharing, truncation).
  • Unmapped errors causing ambiguous failures.

Minimal Concrete Example

#ifdef _WIN32
HANDLE h = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
#else
int fd = open(path, O_RDONLY);
#endif

Common Misconceptions

  • “POSIX and Windows are mostly the same” -> Many behaviors differ.
  • “Path strings are interchangeable” -> Path syntax differs.
  • “Errors map 1:1” -> Many error codes are unique.

Check-Your-Understanding Questions

  1. Why is fork hard to support on Windows?
  2. What is the difference between a file descriptor and a HANDLE?
  3. Why do path semantics matter for portability?

Check-Your-Understanding Answers

  1. Windows does not have fork semantics; CreateProcess is different.
  2. FDs are small integers, HANDLEs are opaque kernel pointers.
  3. Different path rules mean the same string may refer to different files.

Real-World Applications

  • libuv hides OS differences behind a consistent API.
  • Rust standard library implements OS-specific backends.

Where You’ll Apply It

References

  • “Advanced Programming in the UNIX Environment” - POSIX APIs.
  • “Windows System Programming” - Windows API basics.

Key Insights

Portability is about designing a consistent subset, not exposing every OS feature.

Summary

Cross-platform abstractions must respect the differences between POSIX and Windows. A clean design chooses a minimal, consistent API and documents any semantic gaps.

Homework/Exercises to Practice the Concept

  1. Write a small wrapper that opens and reads a file on POSIX and Windows.
  2. Add a portable sleep_ms function using nanosleep and Sleep.
  3. Compare error codes for missing files on both platforms.

Solutions to the Homework/Exercises

  1. The wrapper unifies the open/read/close interface.
  2. A portable sleep uses nanosleep on POSIX and Sleep on Windows.
  3. POSIX uses ENOENT, Windows uses ERROR_FILE_NOT_FOUND.

Concept 3: Error Normalization and Feature Detection

Fundamentals

Error handling must be consistent across platforms. POSIX sets errno, Windows uses GetLastError, and the numerical codes do not match. A portable library must translate these into a common error enum. This error enum should capture the most common failures: not found, permission denied, invalid input, timeout, and so on. Some OS errors cannot be mapped exactly; you must choose a fallback category like XPLAT_ERR_UNKNOWN. Feature detection is also part of portability. Some APIs exist only on certain OS versions or require specific permissions. Build-time detection (via CMake or configure scripts) and runtime detection (via probes) are both useful. The library should expose a feature query API so clients can adapt.

Deep Dive into the concept

Error normalization starts with a clear error taxonomy. You define a small, stable set of error codes that cover the common cases. The goal is not to preserve every OS-specific code but to provide predictable behavior. For example, when a file is missing, both POSIX and Windows should return XPLAT_ERR_NOT_FOUND. When permissions are missing, return XPLAT_ERR_PERMISSION. If a call fails for an unexpected reason, return XPLAT_ERR_UNKNOWN and provide a way to query the raw OS error for debugging. This two-level approach gives portability and still allows diagnostics.

The mapping logic must be carefully tested. On POSIX, errno is thread-local and only valid immediately after a failing call. You must read it right away. On Windows, GetLastError is thread-local but similar. The mapping function should be centralized so that all errors go through the same translation. This prevents inconsistent behavior across modules.

Feature detection matters because a portable API is only as good as its platform coverage. You might support pread on POSIX, but Windows has ReadFile with an OVERLAPPED structure for positional reads. If that is not available, you may need to emulate it by seeking and reading, which is not thread-safe. The API should make these limitations explicit. Build-time detection can decide whether to use a function directly or to compile a fallback. Runtime detection can check OS versions or feature flags. For example, Windows has different APIs available on different versions. You can expose a xplat_has_feature function so callers can adjust behavior.

Error handling also influences API design. Functions should return a status code and use output parameters for results. This avoids relying on errno and makes the API explicit. You should document error codes for each function and ensure they are consistent across platforms. For example, xplat_open should always return XPLAT_ERR_NOT_FOUND for a missing file on both OSes. The mapping table must be part of your documentation so users know what to expect.

Finally, error normalization interacts with testing. You should include tests that simulate each error on both platforms. That can be done by trying to open a missing file, creating a file in a read-only directory, or passing invalid parameters. These tests verify that the error mapping is consistent. Feature detection should also be tested: for example, you can compile with and without a specific API and verify that the fallback path works.

How This Fits on Projects

This concept defines the error enum in Section 3.2 and the mapping module in Section 4.2. It is also relevant to error handling in P01-custom-memory-allocator.md and to deterministic failures in P06-embedded-key-value-database.md.

Definitions & Key Terms

  • Error normalization: Mapping OS-specific errors to a common error set.
  • Feature detection: Determining available APIs at build or runtime.
  • Fallback implementation: A portable alternative when an API is missing.
  • Status code: Return value indicating success or failure.

Mental Model Diagram (ASCII)

POSIX errno -> map -> XPLAT_ERR_* <- map <- Windows GetLastError

How It Works (Step-by-Step)

  1. Call OS API.
  2. On failure, fetch errno or GetLastError immediately.
  3. Map to xplat_error_t using a centralized table.
  4. Return status code and optionally store raw OS error.

Invariants:

  • Every failing call maps to a defined error code.
  • Raw OS error is optional but preserved for debugging.

Failure modes:

  • Forgetting to capture errno early results in wrong error.
  • Inconsistent mapping between modules.

Minimal Concrete Example

typedef enum {
    XPLAT_OK = 0,
    XPLAT_ERR_NOT_FOUND,
    XPLAT_ERR_PERMISSION,
    XPLAT_ERR_INVALID,
    XPLAT_ERR_UNKNOWN
} xplat_error_t;

Common Misconceptions

  • “Returning -1 is enough” -> It hides the reason for failure.
  • “Errors map perfectly” -> Some do not and need a fallback.
  • “Feature detection is optional” -> It prevents runtime crashes.

Check-Your-Understanding Questions

  1. Why should you capture errno immediately after a failure?
  2. Why provide both normalized and raw error codes?
  3. What is the difference between build-time and runtime feature detection?

Check-Your-Understanding Answers

  1. errno can be overwritten by other calls.
  2. Normalized codes give portability; raw codes help debugging.
  3. Build-time decides which code to compile, runtime decides which path to use.

Real-World Applications

  • libuv uses normalized error codes for portability.
  • Database engines map OS errors to portable error enums.

Where You’ll Apply It

  • This project: Section 3.2 Functional Requirements, Section 4.2 Key Components, Section 6.2 Critical Test Cases.
  • Also used in: P06-embedded-key-value-database.md for storage errors.

References

  • “The Linux Programming Interface” - error handling in POSIX.
  • “Windows System Programming” - error codes and mapping.

Key Insights

A portable library must provide stable, meaningful errors even when OS codes differ.

Summary

Error normalization and feature detection are what make a portability layer usable. Without them, behavior differs across platforms and users lose trust in the API.

Homework/Exercises to Practice the Concept

  1. Build a small error mapping table for common POSIX and Windows errors.
  2. Add a function that reports both normalized and raw error codes.
  3. Implement a feature flag for a platform-specific function.

Solutions to the Homework/Exercises

  1. The mapping provides consistent errors like NOT_FOUND and PERMISSION.
  2. Raw codes help debug OS-specific failures.
  3. Feature flags allow safe fallbacks when APIs are missing.

3. Project Specification

3.1 What You Will Build

A portable C library named xplat that provides a small, stable API for file I/O, time, and sleep. The library hides platform differences using opaque types and normalized errors. It includes a demo CLI that performs open/read/stat/sleep operations and prints identical output on Linux and Windows. The scope is intentionally limited to a core subset of system calls to keep behavior consistent.

3.2 Functional Requirements

  1. File API: open, read, write, close, stat.
  2. Time API: monotonic time in nanoseconds.
  3. Sleep API: sleep for a number of milliseconds.
  4. Error API: normalized error enum and raw error retrieval.
  5. Versioning: API version function and build-time version macro.
  6. Demo CLI: cross-platform demo with deterministic output.

3.3 Non-Functional Requirements

  • Portability: Build and run on Linux and Windows.
  • ABI stability: Public headers do not expose struct layouts.
  • Usability: Clear error codes and examples.

3.4 Example Usage / Output

$ ./xplat_demo
[xplat] platform=linux
[xplat] open("data.txt") -> ok
[xplat] read: 128 bytes
[xplat] sleep(50ms) -> ok

3.5 Data Formats / Schemas / Protocols

Error JSON for demo output (if --json flag is used):

{
  "op": "open",
  "status": "error",
  "error": "NOT_FOUND",
  "os_error": 2
}

3.6 Edge Cases

  • Opening a non-existent file returns XPLAT_ERR_NOT_FOUND.
  • Passing NULL path returns XPLAT_ERR_INVALID.
  • Sleep of 0 ms returns immediately.

3.7 Real World Outcome

A library that behaves the same on Linux and Windows for core file operations.

3.7.1 How to Run (Copy/Paste)

make
./xplat_demo

3.7.2 Golden Path Demo (Deterministic)

  • Demo uses a fixed test file and fixed sleep duration.
  • Output is identical across runs.

3.7.3 If CLI: Exact Terminal Transcript

$ ./xplat_demo
[xplat] platform=linux
[xplat] open("data.txt") -> ok
[xplat] read: 128 bytes
[xplat] sleep(50ms) -> ok
exit_code=0

$ ./xplat_demo --file missing.txt
[xplat] open("missing.txt") -> error NOT_FOUND
exit_code=2

3.7.4 If Web App

Not applicable.

3.7.5 If API

Not applicable.

3.7.6 If Library

Install/import

#include "xplat.h"

Minimal usage

xplat_file_t* f = xplat_open("data.txt", &err);
if (f) { xplat_close(f); }

Error handling

xplat_error_t err;
xplat_file_t* f = xplat_open("missing.txt", &err);
if (!f && err == XPLAT_ERR_NOT_FOUND) { /* handle */ }

3.7.7 If GUI / Desktop / Mobile

Not applicable.

3.7.8 If TUI

Not applicable.


4. Solution Architecture

4.1 High-Level Design

+-----------+    +----------------+    +----------------+
| Public API| -> | Platform Layer | -> | OS Syscalls    |
+-----------+    +----------------+    +----------------+

4.2 Key Components

Component Responsibility Key Decisions
Public headers Stable ABI Opaque structs
POSIX backend Implement API on POSIX Use fd-based calls
Windows backend Implement API on Windows Use HANDLE-based calls
Error mapper Normalize errors Central mapping table

4.4 Data Structures (No Full Code)

typedef struct xplat_file {
    void* impl; /* points to fd or HANDLE wrapper */
} xplat_file_t;

4.4 Algorithm Overview

Key Algorithm: open

  1. Validate input path.
  2. Call OS-specific open.
  3. On failure, map error.
  4. On success, wrap handle in opaque struct.

Complexity Analysis:

  • Time: O(1) for open/read/write.
  • Space: O(1) per open file handle.

5. Implementation Guide

5.1 Development Environment Setup

# Linux
make

# Windows (MSVC)
cmake -S . -B build
cmake --build build

5.2 Project Structure

xplat/
|-- include/
|   `-- xplat.h
|-- src/
|   |-- xplat_posix.c
|   |-- xplat_win.c
|   `-- xplat_error.c
|-- tools/
|   `-- xplat_demo.c
`-- Makefile

5.3 The Core Question You’re Answering

“How do you provide a stable API when OS semantics differ?”

5.4 Concepts You Must Understand First

  1. ABI stability and opaque types
  2. POSIX vs Windows I/O semantics
  3. Error normalization and feature detection

5.5 Questions to Guide Your Design

  1. Which APIs are in scope for v1?
  2. How do you expose errors without leaking OS details?
  3. What features are optional vs required?

5.6 Thinking Exercise

You need to support stat on both platforms.

  • How do you map stat fields to a portable struct?
  • Which fields must you omit?

5.7 The Interview Questions They’ll Ask

  1. What is the difference between API and ABI?
  2. Why should public structs be opaque?
  3. How do you map Windows errors to POSIX-style errors?

5.8 Hints in Layers

Hint 1: Keep API small

Start with open/read/write/close and sleep.

Hint 2: Use opaque types

Expose only pointers, not struct fields.

Hint 3: Centralize errors

Map all errors in one file.

Hint 4: Add feature flags

Expose xplat_has_feature().

5.9 Books That Will Help

Topic Book Chapter
Linking and ABI “Computer Systems: A Programmer’s Perspective” Linking chapter
POSIX I/O “Advanced Programming in the UNIX Environment” File I/O
Windows API “Windows System Programming” File I/O

5.10 Implementation Phases

Phase 1: POSIX Backend (1 week)

Goals:

  • Implement basic file and time APIs on POSIX

Tasks:

  1. Write POSIX open/read/write/close wrappers.
  2. Add error mapping for POSIX errors.

Checkpoint: Demo works on Linux.

Phase 2: Windows Backend (1 week)

Goals:

  • Implement Windows equivalents

Tasks:

  1. Map to CreateFile/ReadFile/WriteFile.
  2. Map Windows errors.

Checkpoint: Demo works on Windows.

Phase 3: ABI and Polishing (1 week)

Goals:

  • Harden ABI and document behavior

Tasks:

  1. Review headers for exposed structs.
  2. Add versioning and feature detection.

Checkpoint: API stable, tests pass on both platforms.

5.11 Key Implementation Decisions

Decision Options Recommendation Rationale
Error model errno passthrough vs normalized enum normalized enum Consistency
File handle expose fd vs opaque opaque ABI safety
Build system Makefile vs CMake CMake for Windows Cross-platform

6. Testing Strategy

6.1 Test Categories

Category Purpose Examples
Unit Tests Error mapping missing file errors
Integration Tests Cross-platform behavior open/read/write
Edge Case Tests Invalid inputs NULL path

6.2 Critical Test Cases

  1. Missing file maps to XPLAT_ERR_NOT_FOUND on both platforms.
  2. Invalid parameters map to XPLAT_ERR_INVALID.
  3. Version API returns expected string.

6.3 Test Data

file: data.txt
bytes: 128
sleep_ms: 50

7. Common Pitfalls & Debugging

7.1 Frequent Mistakes

Pitfall Symptom Solution
Exposed structs ABI break Use opaque types
Inconsistent errors Different behavior Central error mapping
Platform ifdef chaos Unreadable code Isolate platform files

7.2 Debugging Strategies

  • Build on both platforms early: avoid late surprises.
  • Log raw OS errors: validate mapping tables.

7.3 Performance Traps

  • Excessive conversions between path formats.
  • Using blocking I/O in performance-critical paths.

8. Extensions & Challenges

8.1 Beginner Extensions

  • Add a portable directory listing API.
  • Add a simple environment variable API.

8.2 Intermediate Extensions

  • Add socket abstraction.
  • Add asynchronous file I/O layer.

8.3 Advanced Extensions

  • Add ABI compatibility tests with an external tool.
  • Support Unicode path normalization.

9. Real-World Connections

9.1 Industry Applications

  • libuv: Cross-platform asynchronous I/O.
  • Rust std: OS-specific backends under a stable API.
  • libuv: Event loop and portability layer.
  • APR: Apache Portable Runtime.

9.3 Interview Relevance

  • Explain ABI vs API differences.
  • Describe how to design stable C interfaces.

10. Resources

10.1 Essential Reading

  • “Computer Systems: A Programmer’s Perspective” - linking and ABI.
  • “Windows System Programming” - Windows API basics.

10.2 Video Resources

  • Talks on ABI stability and cross-platform development.

10.3 Tools & Documentation

  • abi-compliance-checker for ABI checks.
  • cmake for cross-platform builds.

11. Self-Assessment Checklist

11.1 Understanding

  • I can explain why ABI stability matters.
  • I can describe differences between POSIX and Windows I/O.
  • I can map OS errors to a stable enum.

11.2 Implementation

  • The library builds on Linux and Windows.
  • Opaque types are used for all public handles.
  • Error mapping behaves consistently.

11.3 Growth

  • I can explain API design choices in an interview.
  • I can extend the library without breaking ABI.

12. Submission / Completion Criteria

Minimum Viable Completion:

  • File open/read/write and sleep APIs implemented.
  • Demo works on one platform.

Full Completion:

  • Demo works on Linux and Windows with identical output.
  • Error mapping is tested.

Excellence (Going Above & Beyond):

  • ABI compatibility checks integrated into CI.
  • Extended API for sockets or directories.