← Back to all projects

SPRINT 4 BOUNDARIES INTERFACES PROJECTS

Learning “Boundaries & Interfaces” in C Through Real-World Projects

Core Concept Analysis

The essence of this topic is understanding that every function signature is a contract, every header is a legal document, and every boundary between modules is where bugs breed. Let me break down the fundamental building blocks:

Concept What You Must Internalize
Translation Units A .c file + all its includes = one compilation unit. Linking is where boundaries meet.
Headers as Contracts A .h file declares promises. Implementation details stay hidden.
Opaque Structs typedef struct Foo Foo; in header, full definition only in .c
Ownership Who allocates? Who frees? Who can mutate? This must be OBVIOUS from the API.
ABI vs API API = source compatibility. ABI = binary compatibility. Breaking either has different costs.
Const-correctness const isn’t just documentation—it’s a compiler-enforced promise about mutation
Defensive APIs Design so misuse is impossible, not just documented

Project 1: Plugin System for a Simple Shell

  • File: SPRINT_4_BOUNDARIES_INTERFACES_PROJECTS.md
  • Programming Language: C
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Systems Programming / API Design
  • Software or Tool: dlopen / Plugins
  • Main Book: “C Interfaces and Implementations” by David R. Hanson

What you’ll build: A minimal Unix shell that loads plugins (commands) at runtime from .so files. Plugins can register new commands like hello, calc, or uptime that users can invoke.

Why it teaches Boundaries & Interfaces: This project forces you to confront the hardest boundary in C—the boundary between your code and code that doesn’t exist yet. You’ll design an interface that unknown future code must implement. Get it wrong, and plugins crash your shell. Get it right, and you’ve mastered stable API design.

Core challenges you’ll face:

  • Defining a plugin interface (struct of function pointers) that’s stable across versions (ABI vs API)
  • Using opaque handles so plugins can’t corrupt shell internal state (opaque structs)
  • Passing data to plugins without giving them ownership they’ll abuse (ownership across boundaries)
  • Versioning your plugin API so old plugins don’t crash new shells (designing minimal interfaces)
  • Keeping plugin errors from crashing the host process (defensive APIs)

Key Concepts:

  • Translation units & linking: “C Interfaces and Implementations” by David R. Hanson - Chapter 1
  • Opaque types and encapsulation: “Effective C, 2nd Edition” by Robert C. Seacord - Chapter 5 (Types)
  • Dynamic loading in C: “The Linux Programming Interface” by Michael Kerrisk - Chapter 41 (dlopen/dlsym)
  • ABI stability: “Advanced C and C++ Compiling” by Milan Stevanovic - Chapter 11

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Comfortable with pointers, structs, basic Unix (fork/exec)

Real world outcome:

  • A working shell where you can type plugin load ./hello.so and then hello World prints “Hello, World!”
  • You can add new commands without recompiling the shell
  • Plugins can be unloaded and reloaded at runtime

Learning milestones:

  1. After implementing plugin loading with dlopen/dlsym — you understand how translation units become shared objects and how symbols are resolved at runtime
  2. After handling a plugin that passes bad data — you understand why defensive APIs validate everything at boundaries
  3. After adding versioning to your plugin struct — you understand why ABI stability matters and how to design for forward compatibility

Project 2: Key-Value Store Client Library

  • File: SPRINT_4_BOUNDARIES_INTERFACES_PROJECTS.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Python
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: Level 4: The “Open Core” Infrastructure
  • Difficulty: Level 2: Intermediate (The Developer)
  • Knowledge Area: Networking, API Design
  • Software or Tool: Redis
  • Main Book: C Interfaces and Implementations by David Hanson

What you’ll build: A C client library (like libkvclient) that connects to Redis or your own simple TCP key-value server. The library exposes kv_connect(), kv_set(), kv_get(), kv_disconnect() and handles all protocol details internally.

Why it teaches Boundaries & Interfaces: Client libraries are the purest form of “boundary as contract.” Your users see only your .h file. They never see your parsing logic, socket handling, or internal buffers. You must communicate ownership clearly: who owns the returned string from kv_get()? The caller? The library? This ambiguity has caused thousands of real-world bugs.

Core challenges you’ll face:

  • Designing kv_handle as an opaque type (opaque structs, encapsulation)
  • Deciding who owns memory returned by kv_get() and making it obvious (ownership across boundaries)
  • Using const char* correctly for keys vs mutable buffers for values (const-correctness)
  • Hiding protocol details while allowing configuration (internal vs external invariants)
  • Handling connection state without exposing it (avoiding global state)

Key Concepts:

  • Opaque pointers pattern: “C Interfaces and Implementations” by David R. Hanson - Chapter 2
  • Memory ownership in APIs: “Effective C, 2nd Edition” by Robert C. Seacord - Chapter 6 (Memory Management)
  • Const correctness: “C Programming: A Modern Approach” by K. N. King - Chapter 17
  • Socket programming: “The Linux Programming Interface” by Michael Kerrisk - Chapters 56-59
  • Defensive coding: “Code Complete, 2nd Edition” by Steve McConnell - Chapter 8 (Defensive Programming)

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Basic socket programming, memory management

Real world outcome:

kv_handle *db = kv_connect("localhost", 6379);
kv_set(db, "user:1:name", "Alice");
char *name = kv_get(db, "user:1:name");  // Clear who owns this!
printf("Name: %s\n", name);
kv_free_string(name);  // Explicit ownership transfer
kv_disconnect(db);

Your library connects to a real Redis server (or your mock), and the API makes ownership unmistakable.

Learning milestones:

  1. After implementing kv_connect() returning an opaque handle — you understand why hiding struct internals prevents misuse
  2. After struggling with “who frees the returned string” — you’ll never design an ambiguous ownership API again
  3. After adding error handling that doesn’t expose internal errno — you understand the boundary between internal and external invariants

Project 3: JSON Parser Library

  • File: SPRINT_4_BOUNDARIES_INTERFACES_PROJECTS.md
  • Main Programming Language: C
  • Alternative Programming Languages: Rust, Go, Zig
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: Level 4: The “Open Core” Infrastructure
  • Difficulty: Level 3: Advanced (The Engineer)
  • Knowledge Area: Parsing, API Design
  • Software or Tool: JSON Parser Library
  • Main Book: Language Implementation Patterns by Terence Parr

What you’ll build: A JSON parsing library (libtinyjson) with an API like: json_parse(), json_get_string(), json_get_number(), json_array_get(), json_free(). The user never sees your internal node structures.

Why it teaches Boundaries & Interfaces: JSON is hierarchical—objects contain objects contain arrays contain values. Ownership becomes a nightmare: if I call json_get_object(root, "user"), do I own that object? Can the root be freed while I hold a reference? You’ll learn to design APIs where these questions have obvious answers.

Core challenges you’ll face:

  • Making json_value opaque while supporting type introspection (opaque structs, forward declarations)
  • Designing accessors that can’t return dangling pointers (defensive APIs)
  • Using const to indicate “borrowed” vs “owned” returns (const-correctness, ownership)
  • Keeping the parser state internal (encapsulation)
  • Creating a minimal API that handles all JSON types (designing minimal interfaces)

Key Concepts:

  • Handle-based APIs: “C Interfaces and Implementations” by David R. Hanson - Chapter 2
  • Recursive data structure ownership: “Expert C Programming” by Peter van der Linden - Chapter 8
  • Type-safe accessors in C: “Fluent C” by Christopher Preschern - Chapter 5 (Function-based error handling)
  • Parser design: “Language Implementation Patterns” by Terence Parr - Chapter 2

Difficulty: Intermediate to Advanced Time estimate: 2 weeks Prerequisites: Recursion, dynamic memory, string handling

Real world outcome:

json_value *root = json_parse("{\"name\": \"Alice\", \"age\": 30}");
const char *name = json_get_string(root, "name");  // const = borrowed!
printf("Name: %s\n", name);
// name is valid until json_free(root)
json_free(root);
// Using 'name' here would be UB - and a careful user knows because it was 'const'

Learning milestones:

  1. After implementing the opaque json_value with type tags — you understand how to hide implementation while exposing safe accessors
  2. After deciding between “copy-out” vs “borrow” semantics for strings — you understand ownership conventions and how const communicates them
  3. After writing the header file documentation — you’ll appreciate why “headers as contracts” means every function signature tells a story

Project 4: Logging Library with Zero Global State

  • File: SPRINT_4_BOUNDARIES_INTERFACES_PROJECTS.md
  • Programming Language: C
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Systems Programming / Library Design
  • Software or Tool: Logging Framework
  • Main Book: “Clean Code” by Robert C. Martin (Principles apply)

What you’ll build: A logging library (liblog) where each “logger” is an independent instance with its own outputs, formats, and levels. No global log() function. Multiple loggers can coexist without interference.

Why it teaches Boundaries & Interfaces: Most logging libraries cheat with global state (log_init() once, log() everywhere). You’ll design one where the logger handle is explicit everywhere. This forces you to think about how state crosses function boundaries and how to design APIs that don’t hide dependencies.

Core challenges you’ll face:

  • Resisting the temptation to use global state (avoiding global state)
  • Passing logger handles without cluttering every function signature (designing minimal interfaces)
  • Allowing custom output handlers (file, stderr, network) via function pointers (encapsulation, callbacks across boundaries)
  • Making the logger thread-safe without exposing mutexes (internal vs external invariants)
  • Ensuring format strings can’t cause crashes (defensive APIs, const-correctness)

Key Concepts:

  • Avoiding global state: “Clean Code” by Robert C. Martin - Chapter 5 (Formatting), and general OOP principles applied to C
  • Function pointer callbacks: “Expert C Programming” by Peter van der Linden - Chapter 9
  • Thread-safe C design: “The Linux Programming Interface” by Michael Kerrisk - Chapter 31
  • Dependency injection in C: “Fluent C” by Christopher Preschern - Chapter 12 (Dependency Inversion)

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Basic C, understanding of function pointers

Real world outcome:

log_config cfg = { .level = LOG_DEBUG, .output = stderr };
logger *app_log = log_create(&cfg);
logger *net_log = log_create(&(log_config){ .level = LOG_ERROR, .output = netfile });

log_info(app_log, "Application started");
log_error(net_log, "Connection failed: %s", strerror(errno));

log_destroy(app_log);
log_destroy(net_log);

Two independent loggers, no global state, clear ownership.

Learning milestones:

  1. After implementing without any static variables — you understand how global state creates hidden dependencies
  2. After adding custom output handlers — you understand function pointers as boundaries and the contracts they require
  3. After writing a multi-threaded test with two loggers — you understand internal invariants that callers shouldn’t need to know about

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
Plugin System (Shell) Intermediate 1-2 weeks ⭐⭐⭐⭐⭐ (ABI, opaque types) ⭐⭐⭐⭐
Key-Value Client Library Intermediate 1-2 weeks ⭐⭐⭐⭐ (ownership, headers) ⭐⭐⭐
JSON Parser Library Intermediate-Advanced 2 weeks ⭐⭐⭐⭐⭐ (ownership hierarchy) ⭐⭐⭐⭐
Logging Library Intermediate 1 week ⭐⭐⭐ (global state, callbacks) ⭐⭐⭐

Recommendation

Start with: Key-Value Client Library

Here’s why:

  1. It’s the most practical — you’ll have a working Redis client
  2. Ownership is crystal clear (connect/disconnect, get/free patterns)
  3. Headers as contracts is immediately applicable (your .h IS your documentation)
  4. It’s not too complex to finish in a week while still hitting all core concepts

Then move to: JSON Parser Library

This builds on ownership concepts but adds the complexity of hierarchical data. The question “do I own this nested value?” will haunt you until you solve it elegantly.

Finally: Plugin System

This is the capstone. ABI vs API, version compatibility, and the ultimate trust boundary (running unknown code). If you can design a plugin system that doesn’t crash when loaded with a malformed plugin, you’ve truly mastered defensive interfaces.


Final Comprehensive Project: Build libhttp-lite

What you’ll build: A minimal HTTP/1.1 client library inspired by libcurl’s design philosophy. Supports GET/POST, custom headers, response parsing, and callbacks for streaming responses.

Why it’s the ultimate boundary test: This combines everything:

  • Opaque handles: Connection handle, request handle, response handle
  • Complex ownership: Who owns the response body? Headers? Connection after reuse?
  • Callbacks across boundaries: Progress callbacks, write callbacks, header callbacks
  • Const-correctness: Request headers are const, response bodies are mutable
  • ABI considerations: Adding new options without breaking existing code
  • Defensive APIs: Invalid URLs, malformed responses, timeout handling
  • Multiple translation units: Parser, socket layer, URL handling, TLS (optional)

Core challenges you’ll face:

  • Designing an “easy” vs “multi” interface (simple vs power user APIs)
  • Options pattern: http_setopt(handle, OPTION_TIMEOUT, 30) without type safety
  • Callback contracts: What guarantees do you make about when callbacks fire?
  • Memory strategy: Small responses buffered, large responses streamed
  • Error propagation: How do internal socket errors become user-visible errors?

Key Concepts:

  • libcurl architecture study: Read libcurl’s design docs — it’s the gold standard
  • Options pattern in C: “C Interfaces and Implementations” by David R. Hanson - general patterns
  • HTTP protocol: RFC 7230-7235 (read the actual specs!)
  • Connection pooling: “High Performance Browser Networking” by Ilya Grigorik - Chapter 11

Difficulty: Advanced Time estimate: 3-4 weeks Prerequisites: Completed 2-3 projects above, comfortable with sockets and parsing

Real world outcome:

http_handle *h = http_easy_init();
http_setopt(h, HTTP_URL, "https://api.github.com/users/torvalds");
http_setopt(h, HTTP_WRITEFUNCTION, my_write_callback);
http_setopt(h, HTTP_WRITEDATA, &my_buffer);

http_result res = http_perform(h);
if (res == HTTP_OK) {
    printf("Response: %s\n", my_buffer.data);
}

http_cleanup(h);

You can curl any HTTP API from C with a library you designed and built.

Learning milestones:

  1. After designing the handle types — you’ve internalized “users see handles, not structs”
  2. After implementing streaming callbacks — you understand callback contracts deeply
  3. After adding a second HTTP method without changing existing code — you’ve designed for extensibility
  4. After handling malformed server responses gracefully — you’ve built truly defensive APIs
  5. After writing the public header — you can look at any C library’s .h and understand the design decisions (and mistakes) they made

Final Thought

After completing these projects, go read the header files of libcurl, sqlite3, or OpenSSL. You’ll suddenly see the thousands of micro-decisions: why this returns const, why that takes a callback, why this handle is opaque. What once seemed arbitrary will reveal itself as battle-tested interface design.

The curse of C is that the language doesn’t enforce boundaries. The blessing is that once you learn to build them yourself, you’ll design better interfaces in any language.