For those of us who are in the no-ABI-breakage business (I used to be when I worked on JavaScriptCore, which is a framework on Apple platforms and folks dynlink to it), any behavior change that causes an app that has nontrivial number of users to break is considered an ABI break.
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.
> 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.
One big thing to watch out for is structs. You can’t add extra members later.
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.
The solution in old Win32 APIs was to have a length field as the first member of the struct. The client sets this to sizeof(the_struct). As long as structs are only ever extended the library knows exactly which version it is dealing with from the length.
This got a bit messy because Windows also included compatibility hacks for clients that didn't set the length correctly.
> 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?
> What is a solution to this? If I cannot expose structs that might grow what do I expose then?
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.
Unless I've been wrong all these years in C you can write a header file which says this is a pointer to T, without ever saying what's in T and C will not allow people to access the elements of T, since it doesn't know what they are, but they can have the pointer since it doesn't need to know how T is laid out to know it's a pointer and all pointers are laid out the same in C.
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.
> in C you can write a header file which says this is a pointer to T, without ever saying what's in T
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.
No, I don't mean void pointers, I'm talking about just pointers to some unknown type T.
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.
Next biggest humanity-wide problem in this area will be 32/64 bit time I believe. We store timestamps as seconds since 1.01.1970 00:00 UTC, and it turns out someone thought 32 bits will be enough for everybody, so the counter will wrap some time in 2038 year, which is less than 14 years in the future. How do you fix the fact that, on 32-bit systems (in x86 it's limited to i386/i686, which is less and less common, but the world is not only x86) time-related functions return 32-bit wide variables? You either return wrong values or you break the ABI.
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 alter signatures or data structures in the published header file, there is breakage.
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.
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.