Project 5: Crafting a Stable ABI with Opaque Pointers

A C shared library for a “counter” object. The key is that the public header file (counter.h) will not define the struct Counter—it will only forward-declare it. This makes the pointer “opaque”. You will then provide a V2 of your library with a completely different internal struct that is still 100% binary compatible with the original.

Quick Reference

Attribute Value
Primary Language C
Alternative Languages C++
Difficulty Level 3: Advanced
Time Estimate 1-2 weeks
Knowledge Area API Design / Library Development
Tooling Shared libraries (.so/.dll)
Prerequisites C pointers, dynamic memory, experience building shared libraries.

What You Will Build

A C shared library for a “counter” object. The key is that the public header file (counter.h) will not define the struct Counter—it will only forward-declare it. This makes the pointer “opaque”. You will then provide a V2 of your library with a completely different internal struct that is still 100% binary compatible with the original.

Why It Matters

This project builds core skills that appear repeatedly in real-world systems and tooling.

Core Challenges

  • Designing an opaque API → maps to only exposing pointers to incomplete types in headers
  • Managing object lifecycle → maps to providing create, destroy, get, set functions
  • Separating public header from internal implementation → maps to strict discipline in your code structure
  • Proving binary compatibility → maps to swapping the .so file for a new version and having an old program still work

Key Concepts

  • Opaque Pointer (PIMPL Idiom): A widely used technique to hide private data members from a class or interface.
  • API vs. ABI: The API is the source-level contract; the ABI is the binary-level contract. This project shows how to change the former slightly while preserving the latter.
  • Dynamic Linking: How an executable finds and uses a .so file at runtime.

Real-World Outcome

struct counter; // Incomplete type -> Opaque
typedef struct counter counter_t;

counter_t* counter_create(int initial_value);
void counter_destroy(counter_t* c);
void counter_increment(counter_t* c);
int counter_get_value(const counter_t* c);

Implementation Guide

  1. Reproduce the simplest happy-path scenario.
  2. Build the smallest working version of the core feature.
  3. Add input validation and error handling.
  4. Add instrumentation/logging to confirm behavior.
  5. Refactor into clean modules with tests.

Milestones

  • Milestone 1: Minimal working program that runs end-to-end.
  • Milestone 2: Correct outputs for typical inputs.
  • Milestone 3: Robust handling of edge cases.
  • Milestone 4: Clean structure and documented usage.

Validation Checklist

  • Output matches the real-world outcome example
  • Handles invalid inputs safely
  • Provides clear errors and exit codes
  • Repeatable results across runs

References

  • Main guide: LEARN_C_ABI_DEEP_DIVE.md
  • “C Interfaces and Implementations” by David R. Hanson