|
The problem comes from what happens when you're trying to use LibDependency from your own code, say for example you're have something like: LibDependency.h:
typedef struct SomeOpaqueLibDependencyType * SomeOpaqueLibDependencyTypeRef;
SomeOpaqueLibDependencyTypeRef MakeTheThing();
void UseTheThing(SomeOpaqueLibDependencyTypeRef);
LibraryA:
...
SomeOpaqueLibDependencyTypeRef getSomething();
LibraryB:
...
void doSomething(SomeOpaqueLibDependencyTypeRef);
MyAwesomeApp:
LibraryB.doSomething(LibraryA.getSomething()) // pseudocode
The problem is the because LibraryA and LibraryB have distinct copies of LibDependency, the source/api compatible type you're using may have an incompatible internal structure.As a library author there are things you can do for ABI compatibility, but they all basically boil down to providing a bunch of non-opaque API types that have some kind of versioning (it's either an explicit version number, or it's a slightly implicit size field which IIRC is the MS standard). You also have opaque types where the exposure of details is more more restricted, generally either just an opaque pointer, or maybe a pointer to a type that has a vtable (either an automatic one or a manually constructed one). In general use of the non-opaque portions of the API are fairly restricted because they have ABI implications, so a user of a library will communicate by providing those non opaque data to the library, but the library will provide largely opaque results with an ABI stable API that can be used to ask questions of an otherwise opaque type. This works in general, and it means you don't have to rebuild everything from scratch any time you update anything. It breaks down however when you have different versions of the same library in the same process. The problem is that while you see a single opaque type, it's not opaque to the library itself so while an opaque type from two different versions of a library may look the same to you, the implementation may differ between the two versions. Take a hypothetical: LibDependency.h:
typedef struct OpaqueArray *ArrayRef;
struct ArrayCallbacks {
int version;
size_t elementSize;
void (*destroyElement)(void *);
void (*copyElement)(void *, const void*);
};
ArrayRef ArrayCreate(const ArrayCallbacks*, size_t);
void ArraySet(ArrayRef, size_t, void*);
void *ArrayGet(ArrayRef, size_t);
which is a kind of generic vaguely ABI stable looking thing (I'm in a comment box, assume real code would have more thought/have fewer errors), but lets imagine the a "plausible" v1.0 LibDependency-1.0.c:
struct InternalArrayCallbacks {
void (*destroyElement)(void *);
void (*copyElement)(void *, const void*);
};
struct OpaqueArray {
InternalArrayCallbacks callbacks;
size_t elementSize;
char buffer[];
}
ArrayRef ArrayCreate(const ArrayCallbacks* callbacks, size_t size) {
size_t allocationSize = sizeof(OpaqueArray) + size * callbacks->elementSize;
OpaqueArray *result = (OpaqueArray *)malloc(allocationSize);
/* initialize result, copy appropriate callbacks, etc */
return result;
}
void ArraySet(ArrayRef array, size_t idx, void* value) {
array->callbacks.copyElement(array->buffer + idx * array->elementSize, value);
}
etc.Now v1.1 says "oh maybe we should bounds check": LibDependency-1.1.c:
...
struct OpaqueArray {
InternalArrayCallbacks callbacks;
size_t elementSize;
size_t maxSize;
char buffer[];
}
...
void ArraySet(ArrayRef array, size_t idx, void* value) {
if (idx >= array->maxSize) abort();
array->callbacks.copyElement(array->buffer + idx * array->elementSize, value);
}
There's been no source change, no feature change, and from a library/OS implementors PoV no ABI change, but if I had an ArrayRef from the 1.0 implementation and passed it somewhere that would be using the 1.1 implementation, or vice versa, the result would be sadness.As a library implementor there's a lot you have to do and/or think about to ensure ABI stability, and it is manageable, but more or less all of the techniques break down when the scenario is "multiple versions of the same library inside a single process". |