1

Some time ago I had asked whether adding fields to a struct of a shared library would require a major or a minor change in the version string according to semantic versioning. The few participants where inclined to suggest a major change.

Months have passed and I have not added any fields to my struct yet. But now it might be the right time for doing it.

However in the meanwhile I have also been thinking about how this could break binary compatibility with programs compiled with previous versions of my library, and honestly, for as much as I have thought, I have not found any possible scenario where binary compatibility breaks.

The new fields are added to the end of the structure, and who uses my library normally does not allocate this struct him-/herself, but receives a pointer to it as an argument of a callback function. This is all my library does with it, the user never needs to call the library back using this struct.

The possible scenarios are two when an old program runs with the new version of the library:

  1. Under normal circumstances (i.e., the old program receives a pointer to this struct and reads the fields it needs), my library will send from now on a pointer to a bigger struct, but the old program does not know it and doesn't care.
  2. If the old program, for some unknown reasons, allocates this struct for its own sake, it will still allocate a smaller structure (that's what the program knew when it was compiled indeed), but my library doesn't know it and doesn't care, since it never needs to receive this struct back from the user, it only sends it as a pointer.

In the previous discussion someone had also posted this quotation from Linux Program Library HOWTO — §3.6. Incompatible Libraries:

When a new version of a library is binary-incompatible with the old one the soname needs to change. In C, there are four basic reasons that a library would cease to be binary compatible:

  1. The behavior of a function changes so that it no longer meets its original specification,

  2. Exported data items change (exception: adding optional items to the ends of structures is okay, as long as those structures are only allocated within the library).

  3. An exported function is removed.

  4. The interface of an exported function changes.

The case no. 2 seems to talk about opaque structures. My struct is not opaque, but the situation is very similar. But the real point is that, as I said, I have not been able to find one single scenario where binary compatibility will be broken after this change.

So the question remains the same: Is this a a minor or a major version change?

madmurphy
  • 1,451
  • 11
  • 20
  • Your choice. The change warrants a major version change despite/ because of point 2. You seem reluctant to make a major version change— but you’ve not explained why? It’s mostly arbitrary. I’d go with major, but your argument for minor also holds up AFAICS. – Jonathan Leffler Sep 20 '19 at 16:11
  • @Johnathan In the previous discussion I hadn't explained many details, and _normally_ adding fields to a `struct` does require a major version change. However, luckily, not in this case. And if the new version is both source and binary compatible with previous versions, in my opinion it is semantic versioning itself that says that I should increase just the minor version number and not the major one. I mean, is not that the whole point of semantic versioning, reading a version change and understanding precisely what it implies? – madmurphy Sep 20 '19 at 16:44
  • As I said, your choice. The decision may hinge on non-technical (non-semantic) issues — how will your customer base take a change of major version rather than minor version; will your marketing department prefer a change of major version to permit a more exciting blurb about ongoing development, etc. Do you want to, and can you technically, decouple the shared object versioning from the marketing versioning? At this point, you need to make a decision. It is unlikely that your customers will complain whichever you choose. – Jonathan Leffler Sep 20 '19 at 20:12
  • Do you provide a function to free the allocated structure, or is the allocated structure always allocated in a single unit so that a simple call to `free()` suffices. Should you provide such a function? That allows you more flexibility in future. However, its presence or absence isn't necessarily a controlling factor. And 'old' program could still allocate a copy of the structure and use it and free it. There would be major issues if the extra fields require auxilliary allocations — I assume, therefore, that the extra fields are simple non-pointer fields. – Jonathan Leffler Sep 20 '19 at 20:17
  • It is [a FOSS project](https://github.com/madmurphy/libconfini), so I am free to do as I like. However, as a general rule, I think that in library versioning you have much less freedom compared to program versioning. When you write a library other programs will depend on you, and versioning must become a reliable mechanism to express what release they can link their binaries to. If you increase the major version number without breaking binary compatibility you are basically telling old programs “Do not use the latest version, even if theoretically you could”. But this is just my personal view. – madmurphy Sep 21 '19 at 12:13
  • About freeing the allocated memory, the library does everything automatically. The user needs only to read/copy what (s)he needs, and that's all. The extra fields are non-pointer fields by the way. – madmurphy Sep 21 '19 at 12:14
  • 1
    So, the answer you want someone to give you is “a change in minor version (only) is OK”. There it is, in words of not more than two syllables. That was also in my first comment, though not as explicitly. – Jonathan Leffler Sep 21 '19 at 15:01

2 Answers2

1

The only issue I can see is if the user does something that relies on the size of your structure and then unexpected results occur when the program is recompiled without the code being updated. Most likely to be affected would be a non-binary distribution like Gentoo or maybe a Yocto build chain.

For example, if the user were to write the contents of an array of these structures to a file that it expects to read next time the program runs. If the program gets recompiled against the new version of the library, there will be issues if it is not aware of the change between versions. Another issue may occur if bad practices like not using sizeof are present.

Graeme
  • 2,971
  • 21
  • 26
  • Thank you for the addition. If they _do_ use `sizeof`, I don't see how binary incompatibility could arise. If the old program gets recompiled it will automatically align everywhere to the new size (thanks to `sizeof`); while if the program was compiled before this release, it will always deal with a truncated version of the `struct` – but that's OK, since a truncated `struct` is exactly what the old program needed. The only possible break I see would come from writing the size of the `struct` by hand. But that can cause incompatibility between machines even without updating the library at all! – madmurphy Sep 20 '19 at 18:59
0

It's a major change. SemVer says nothing about about binary compatibility.

Your customers could be saving the data to a file. If it's a binary file, then the record size has just changed. How does a reader know which file records are old format and which ones are new format? How does the program know there's more than one kind of that struct type?

What if code written and compiled against the new definition, encounters data from the old format? Changing the number of fields in a C struct, is absolutely a breaking change wrt SemVer. Now you might slide under the radar in the context of Linux kernel libraries, but be careful that your new code won't ever be used to process the old data format.

jwdonahue
  • 6,199
  • 2
  • 21
  • 43