I ask this because I'd like to know what practices I might want to avoid to guarantee that there is no ABI breakage in my C project.
I ask this because I'd like to know what practices I might want to avoid to guarantee that there is no ABI breakage in my C project.
Some function in your library used to return 42 and now returns 43, and an app with 10,000 users asserted that you returned 42? That's an ABI break.
There are more obvious examples: renaming functions that someone linked to? ABI break. Change the size of a struct in a public header? ABI break, usually (there are mitigations for that one).
The list goes on and Hyrum's law very much applies.
Another way to define ABI compatibility is that folks who promise it are taking the same exact stance with respect to folks who link against them as the stance that Linus takes regarding not breaking userspace: https://lkml.org/lkml/2012/12/23/75
If I promise you ABI compat, and then I make a change and your app breaks, then I'm promising that it's my fault and not yours.
typedef struct {
char name[50];
int age;
} Person;
vs typedef struct {
int age;
char name[50];
} Person;
Basically anything that moves bytes around in memory for data structures that are passed around. Of course any API breakage is also an ABI breakage.I don't think this is true. You can change things on a superficial level in a source language that still compiles down to the same representation in the end.
If you have a struct which might grow, don’t actually make it part of the ABI, don’t give users any way to find it’s size, and write functions to create, destroy and query it.
Thanks! This is very insightful. What is a solution to this? If I cannot expose structs that might grow what do I expose then?
Or is the solution something like I can expose the structs that I need to expose but if I need to ever extend them in future, then I create a new struct for it?
Option 1: If allocating from the heap or somewhere otherwise fixed in place, then return a pointer-to-void (void *) and cast back to pointer-to-your-struct when the user gives it back to you.
Option 2: If allocating from a pool, just return the index.
Then for your internal stuff you define what's inside T and you can use T normally.
Also, even if you're returning an index, learn from Unix's mistake and don't say it's an integer. Give it some other type, even if that type is just an alias for a primitive integer type, because at least you are signalling that these are not integers and you might make a few programmers not muddle these with other integers they've got. A file descriptor is not, in fact, a process ID, a port number, or a retry count, and 5 is only any of those things if you specify which of them it is.
What's inside a T? How can we make one? We don't know, but that's fine since we have been provided with APIs which give us a pointer-to-T and which take a pointer-to-T so it delivers the opacity required.
What if you're providing an API header to an otherwise closed-source binary and you don't want to expose the struct definition?
How would the compiler know the size and alignment required for struct Foo if it never gets to see its definition?
What you're referring to is a forward declaration. Forward declarations only work when the definition will be known at some point during compilation.
If you're writing an API header to a binary you're shipping and you don't want to provide the definition to the user of the API, then a forward declaration is no bueno. The compiler needs to know the size, alignment, etc.
This got a bit messy because Windows also included compatibility hacks for clients that didn't set the length correctly.
Debian chose to do both: https://wiki.debian.org/ReleaseGoals/64bit-time . Wherever they could, they recompiled much of the stuff changing package names from libsomething to libsomethingt64, so where they couldn't recompile, the app still "works" (does not segfault), but links with 32-bit library that just gets wrong values. Other distros had flag day, essentially recompiled everything and didn't bother with non-packaged stuff that was compiled against old 32-bit libs, thus breaking ABI.
If you add new signatures or data structures, software compiled against the previous version should still work with the new version.
In my opinion the whole issue is more important on Windows than on Linux. Just recompile the application against the new library or keep both the old and the new soversion around.
Some Linux distributions go into major contortions to make ABI stability work, and still compiled applications that are supposed to work with newer distros crash. It is a waste of resources.