Skip to content

Instantly share code, notes, and snippets.

@CarterLi
Created May 29, 2025 07:25
Show Gist options
  • Save CarterLi/2b9c4817206d2cce5a52e4bcdde5d881 to your computer and use it in GitHub Desktop.
Save CarterLi/2b9c4817206d2cce5a52e4bcdde5d881 to your computer and use it in GitHub Desktop.
Opaque Structs and ABI Compatibility

Modifying members of an opaque struct (reordering, removing, etc.) doesn't break the Application Binary Interface (ABI), which is crucial for maintaining forward compatibility.

The Problem: Non-Opaque Structs

Consider this non-opaque struct definition:

typedef struct MyStruct {
    int i;     // 4 bytes
    float f;   // 4 bytes
} MyStruct;

Users can access members directly:

MyStruct s;
s.i = 10;
s.f = 3.14f;

When compiled, the compiler generates code equivalent to:

*(int*)((uint8_t*)&s + 0) = 10;      // offset 0 for 'i'
*(float*)((uint8_t*)&s + 4) = 3.14f; // offset 4 for 'f'

These memory offsets become hardcoded in the binary. If the library author later reorders the struct members:

typedef struct MyStruct {
    float f;   // now at offset 0
    int i;     // now at offset 4
} MyStruct;

The existing compiled code will incorrectly access:

*(int*)((uint8_t*)&s + 0) = 10;      // writes int to float's location!
*(float*)((uint8_t*)&s + 4) = 3.14f; // writes float to int's location!

This is an ABI break that can cause undefined behavior or crashes.

The Solution: Opaque Structs

An opaque struct hides its internal structure from users:

Header file:

// MyStruct.h
typedef struct MyStruct MyStruct; // Forward declaration only

// Public API functions
MyStruct* MyStruct_new(void);
void MyStruct_setI(MyStruct *s, int i);
void MyStruct_setF(MyStruct *s, float f);
int MyStruct_getI(const MyStruct *s);
float MyStruct_getF(const MyStruct *s);
void MyStruct_destroy(MyStruct *s);

Implementation file:

// MyStruct.c
#include "MyStruct.h"
#include <stdlib.h>

// Internal structure definition
struct MyStruct {
    int i;
    float f;
};

MyStruct* MyStruct_new(void) {
    return calloc(1, sizeof(struct MyStruct));
}

void MyStruct_setI(MyStruct *s, int i) {
    s->i = i;
}

void MyStruct_setF(MyStruct *s, float f) {
    s->f = f;
}

int MyStruct_getI(const MyStruct *s) {
    return s->i;
}

float MyStruct_getF(const MyStruct *s) {
    return s->f;
}

void MyStruct_destroy(MyStruct *s) {
    free(s);
}

MyStruct.c will be compiled into a shared library, say libMyStruct.so, and can be updated independently of the application using it.

Usage:

// main.c
MyStruct *s = MyStruct_new();
MyStruct_setI(s, 10);
MyStruct_setF(s, 3.14f);

printf("i = %d, f = %.2f\n", MyStruct_getI(s), MyStruct_getF(s));

MyStruct_destroy(s);
$ gcc -o main main.c -lMyStruct

After the internal implementation of struct MyStruct is updated, you can freely replace libMyStruct.so without recompiling your application, as long as the public API (MyStruct_* functions) remains unchanged.

Benefits

With opaque structs:

  • Users cannot access members directly, preventing hardcoded offset dependencies
  • All member access goes through library functions compiled into the shared library
  • The library can safely reorder, add, or remove internal members
  • ABI compatibility is maintained as long as the public function signatures remain unchanged
  • When the library is updated, the new member access logic is automatically used

This pattern is widely used in C libraries like FILE* in the standard library, where the internal structure varies between implementations but the API remains consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment