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:
- Design a stable C ABI that can survive refactors.
- Wrap POSIX and Windows system calls behind a unified API.
- Normalize error handling into a portable error model.
- Build a portability layer with clear separation of platform code.
- 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)
- Public header declares
typedef struct xplat_file xplat_file_t;. - Library exposes functions that operate on
xplat_file_t*. - Actual struct definition lives in a private source file.
- 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
- Why are opaque structs used in system libraries?
- How can inline functions cause ABI issues?
- Why should you limit exported symbols?
Check-Your-Understanding Answers
- They allow internal changes without breaking client binaries.
- Client code inlines old logic and will not pick up fixes.
- 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
- This project: Section 3.2 Functional Requirements, Section 4.2 Key Components, Section 5.2 Project Structure.
- Also used in: P01-custom-memory-allocator.md for malloc hooks and P05-high-performance-string-search.md for stable search APIs.
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
- Create a library with a public struct and observe ABI break when you add a field.
- Convert the struct to an opaque type and rebuild.
- Use a version macro and compare at runtime.
Solutions to the Homework/Exercises
- The client crashes or reads incorrect fields after the struct change.
- The opaque type hides changes and preserves ABI stability.
- 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)
- User calls
xplat_open(path). - On POSIX, call
openand wrap fd. - On Windows, call
CreateFileand wrap HANDLE. - 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
- Why is
forkhard to support on Windows? - What is the difference between a file descriptor and a HANDLE?
- Why do path semantics matter for portability?
Check-Your-Understanding Answers
- Windows does not have
forksemantics;CreateProcessis different. - FDs are small integers, HANDLEs are opaque kernel pointers.
- 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
- This project: Section 3.2 Functional Requirements, Section 4.2 Key Components, Section 5.2 Project Structure.
- Also used in: P03-mini-async-runtime.md for pollers and P06-embedded-key-value-database.md for file I/O.
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
- Write a small wrapper that opens and reads a file on POSIX and Windows.
- Add a portable
sleep_msfunction usingnanosleepandSleep. - Compare error codes for missing files on both platforms.
Solutions to the Homework/Exercises
- The wrapper unifies the open/read/close interface.
- A portable sleep uses
nanosleepon POSIX andSleepon Windows. - POSIX uses
ENOENT, Windows usesERROR_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)
- Call OS API.
- On failure, fetch
errnoorGetLastErrorimmediately. - Map to
xplat_error_tusing a centralized table. - 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
errnoearly 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
- Why should you capture
errnoimmediately after a failure? - Why provide both normalized and raw error codes?
- What is the difference between build-time and runtime feature detection?
Check-Your-Understanding Answers
errnocan be overwritten by other calls.- Normalized codes give portability; raw codes help debugging.
- 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
- Build a small error mapping table for common POSIX and Windows errors.
- Add a function that reports both normalized and raw error codes.
- Implement a feature flag for a platform-specific function.
Solutions to the Homework/Exercises
- The mapping provides consistent errors like NOT_FOUND and PERMISSION.
- Raw codes help debug OS-specific failures.
- 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
- File API: open, read, write, close, stat.
- Time API: monotonic time in nanoseconds.
- Sleep API: sleep for a number of milliseconds.
- Error API: normalized error enum and raw error retrieval.
- Versioning: API version function and build-time version macro.
- 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
- Validate input path.
- Call OS-specific open.
- On failure, map error.
- 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
- ABI stability and opaque types
- POSIX vs Windows I/O semantics
- Error normalization and feature detection
5.5 Questions to Guide Your Design
- Which APIs are in scope for v1?
- How do you expose errors without leaking OS details?
- What features are optional vs required?
5.6 Thinking Exercise
You need to support stat on both platforms.
- How do you map
statfields to a portable struct? - Which fields must you omit?
5.7 The Interview Questions They’ll Ask
- What is the difference between API and ABI?
- Why should public structs be opaque?
- 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:
- Write POSIX open/read/write/close wrappers.
- Add error mapping for POSIX errors.
Checkpoint: Demo works on Linux.
Phase 2: Windows Backend (1 week)
Goals:
- Implement Windows equivalents
Tasks:
- Map to CreateFile/ReadFile/WriteFile.
- Map Windows errors.
Checkpoint: Demo works on Windows.
Phase 3: ABI and Polishing (1 week)
Goals:
- Harden ABI and document behavior
Tasks:
- Review headers for exposed structs.
- 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
- Missing file maps to
XPLAT_ERR_NOT_FOUNDon both platforms. - Invalid parameters map to
XPLAT_ERR_INVALID. - 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.
9.2 Related Open Source Projects
- 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-checkerfor ABI checks.cmakefor cross-platform builds.
10.4 Related Projects in This Series
- P03-mini-async-runtime.md: poller abstraction ideas.
- P05-high-performance-string-search.md: stable API design for a library.
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.