40. ABI, linking, and designing a stable C API
40.1 Opaque pointers: the technique behind Section 35's CURL*
// mylib.h — the public header. Note: no struct fields visible at all.
typedef struct MyContext MyContext;
MyContext *mycontext_create(void);
void mycontext_set_option(MyContext *ctx, int key, int value);
void mycontext_destroy(MyContext *ctx);
// mylib.c — the private definition, invisible to anyone who only has the header
struct MyContext {
int option_a;
int option_b;
void *internal_state;
};
MyContext *mycontext_create(void) { return calloc(1, sizeof(struct MyContext)); }
void mycontext_destroy(MyContext *ctx) { free(ctx); }
Because callers never see struct MyContext's layout, you can add
fields, reorder them, or change internal_state's type in a later
release without breaking binaries already linked against the old
version — only mylib.c needs to be recompiled. This is precisely
why curl, SQLite (sqlite3*), and OpenSSL (SSL_CTX*) all expose
opaque pointers instead of public structs: it's the C equivalent of an
interface, and it's the only realistic way to version a C library that
other people link against.
40.2 Symbol visibility: don't export what isn't API
// Only symbols marked default-visibility (or not hidden) end up in the shared library's
// exported symbol table; everything else becomes a true implementation detail.
__attribute__((visibility("default")))
int mylib_public_function(void);
__attribute__((visibility("hidden")))
int mylib_internal_helper(void); // callable within the library, invisible to linkers outside it
clang -fvisibility=hidden -shared -o libmylib.so mylib.c # hide everything by default,
# opt individual symbols back in
Real-world example. This is why a well-built shared library's
nm -D libfoo.so shows only its intended public API, not every static
helper — smaller exported symbol tables load faster, can't be
accidentally depended on by other code, and let the library's authors
freely refactor internals (Section 40.1's whole point) without an API
review.