Modifying members of an opaque struct (reordering, removing, etc.) doesn't break the Application Binary Interface (ABI), which is crucial for maintaining forward compatibility.
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.
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.
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.