A side-note about glibc: I have only ever had problems with glibc versioned symbols. The only thing they more or less do is to pin an old executable to old library versions, and you can supposedly install a newer version of the library. You then save a little bit of disk space.
- It doesn't allow you to install minor versions of the same library at the same time (thats how you end up with all these 1.so, 2.so)
- So if you want a binary to work across multiple distributions, you can build it on a really old CentOS and if you are lucky it will work, but I have no trust in this.
- Forward-compatibility is not really considered, where you update your libraries and your existing app becomes more powerful.
- The dynamic linker doesn't allow you to load two different library versions at the same time (without a lot of contortions). You would think you could do `dlopen` and `dlsym` on two different `.so` files, and then just have separate function pointers to each version's functions. But the linker loves to load all the symbols into a global namespace for some reason.
I'm sure there are ways to overcome all of this, but I feel too young to understand how it got this way and to old to learn it propertly :-P. If I were to design a new system today, I'd probably do something like .NET's global assembly cache: Just dump all library versions in /lib, and have the linker pick the best one at runtime. And make it easy to detect and react to a missing lib at runtime.
> It doesn't allow you to install minor versions of the same library at the same time (thats how you end up with all these 1.so, 2.so)
Minor versions should be ABI-compatible so you should only need the newer of the two. For ABI-incompatible changes, a different SONAME is appropriate, though the convention would be .so.0 -> .so.1 etc.
> So if you want a binary to work across multiple distributions, you can build it on a really old CentOS and if you are lucky it will work, but I have no trust in this.
As far as glibc or other libraries that take ABI stability seriously go, this does work.
> Forward-compatibility is not really considered, where you update your libraries and your existing app becomes more powerful.
Symbol versioning does not prevent you from updating the implementation of old symbol versions in a compatible way.
> The dynamic linker doesn't allow you to load two different library versions at the same time (without a lot of contortions). You would think you could do `dlopen` and `dlsym` on two different `.so` files, and then just have separate function pointers to each version's functions. But the linker loves to load all the symbols into a global namespace for some reason.
Having an option to use a separate linking table for specific dlopen calls would be useful, yes.
> Symbol versioning does not prevent you from updating the implementation of old symbol versions in a compatible way.
As I understand the problem is that if application is compatible with both version X and X+1, then there is no way to compile binary that would preferentially use X+1 when available but also work with X.
So applications can not reap the benefits of new version without dropping support with old version.
What benefits are you talking about. In general benefits not related to the ABI will be gained when updating glibc. For benefits related to the ABI the gains are probably so small that conditionally using the new ABI is likely not worth it in almost all cases.
Libraries loaded with RTLD_LOCAL will still have their symbols resolved against those from already loaded libraries as well as the main executable. Looks like there is dlmopen() in glibc since ~2005 though and there is libcapsule [0] which makes it actually usabel - neat.
> The only thing they more or less do is to pin an old executable to old library versions, and you can supposedly install a newer version of the library. You then save a little bit of disk space.
You also then can remove the old library file and only have to maintain one for security purposes.
If you have old library versions around, especially in a packaged environment like most Linux distros, then the old code will sit there and potentially no one will be paying attention to it anymore. If you're lucky you'll have some security software (e.g., Nessus, OpenVAS) tell you there's a bug/exploit in it.
If you have only one maintained version of the library installed, with the old symbols embedded, then it's more likely to be paid attention to.
I'm not at all against versioning, but found glibc's implementation a source of frustration.
It would be great if you could install Qt 5.9, 5.12, 1.14 in parallel and have apps use the latest version (except for that one app that triggers a bug where you fix it to 5.9). This is an actual problem that occurred at work, I had to statically compile Qt in that case. Glibc's idea of lib versions doesn't help here at all if I understand correctly.
That’s not so much glibc’s fault, but the Unix philosophy of /lib, right? Windows’ “solution” to this is to basically require applications to provide their own runtime (such as Qt, GTK, etc.).
More relevant to the question of libc, the traditional Windows solution was to require applications to distribute or statically compile their own Cruntime. Every release of Visual Studio had its own C runtime. To make matters worse, there was a system C runtime, but most applications didn't link against it, MinGW being a notable exception. AFAIU, this was one of the root culprits for the origin of the notorious DLL Hell. Applications would often crash because libA malloc'd a pointer, which was free'd by libB. That is, there was no shared, global heap. This was a far less obvious pitfall than mixing objects between libA and libB or even libA-v1 and libA-v2. Even if libA-v2 was otherwise backward ABI compatible with libA-v1, if they were built by different versions of Visual Studio you could still end up with heap corruption. Indeed, this could happen if two vendors compiled the exact same source code. If an application install overwrote a library in the shared system folder, boom, applications could begin crashing for no obvious reason.
AFAIU, over the years Windows tried to mitigate this with various hacks for detecting cross-heap pointer freeing. But last time I checked their final approach was to guarantee backward compatibility (including backward heap compatibility) for all future Visual Studio C runtimes; ditto for the system C runtime. IOW, Microsoft committed themselves to maintaining a lot of internal runtime magic to preserve binary compatibility across time, which is functionally what glibc has done using version symbols. Of course, it also became less common on Windows to keep DLLs in shared folders.
The system and the app C runtimes are one and the same in Win10+.
As far as cross-DLL interop: the usual solution was to avoid the C stdlib altogether, and just use the underlying Win32 API functions to manage memory that has to cross the boundary. Or, in the COM land, every object manages its own heap memory, exposing it via the ABI-standard IUnknown::Release.
Disk space is cheap, particularly when we're talking about libs that consume a few hundred kb each.
On the other hand, software distributions should continue to rely on shared libraries, for their own software, but third-party compiled apps that are intended to be cross-distro/cross-arch should try to bundle as much as possible.
This is why I prefer /opt over /usr/local for third-party compiled apps.
Maybe. Apps wouldn't have to bundle their own versions of libraries if those libraries actually cared about backwards compatibility. Bundling is just an unfortunate workaround for libraries continually breaking their ABI.
Windows 10 can run virtually all apps compiled against Windows 2000 just fine, and those apps did not have to bundle their own graphical toolkits. Windows has gone through several new toolkits but they always preserve the old ones so that old programs continue to work.
By contrast GTK has regularly broken things between even minor version updates. Distributions also drop the old major versions of toolkits much more quickly. GTK3 was first released in 2011, but by 2018 most distributions no longer provided GTK2 pre-installed.
Is it any wonder that no one can ship and maintain a binary app that targets GTK without bundling it? Of course bundling sure looks like an attractive solution in this environment, but it's the environment that's the problem.
>Windows 10 can run virtually all apps compiled against Windows 2000 just fine, and those apps did not have to bundle their own graphical toolkits.
They do if they're buildtusing Qt, GTK, WxWidgets, etc. Also shipping DirectX and VC++ runtime libraries, .NET runtimes, etc. was and still is (for whatever reason) still common and stuff just doesn't work without it. Plus whatever else the program needs, like a whole python runtime or something.
>Is it any wonder that no one can ship and maintain a binary app that targets GTK without bundling it?
Distributions have no problem doing it. If you're shipping something outside the distribution why would you ever expect that shipping only half your program would be feasible? There is no OS where that works.
Windows has many solutions to this. It is entirely possible for the apps to use a shared dynamically linked runtime, and the OS provides mechanisms to have several versions installed globally side by side, loading the correct one as needed for each app.
> It would be great if you could install Qt 5.9, 5.12, 1.14 in parallel and have apps use the latest version (except for that one app that triggers a bug where you fix it to 5.9).
The underlying library-loading system on Linux handles this just fine, and has for decades (look up "soname" for details).
The problem is that system package managers want to load "just the latest" & maintainers have to take extra steps to enable loading of multiple versions.
The “compile in an old version” thing was IIRC more properly done with linker scripts telling the linker to use an older symbol. I think lsb-gcc does it for you — not that the Linux Standard Base is still widely respected at all.
> So if you want a binary to work across multiple distributions, you can build it on a really old CentOS and if you are lucky it will work, but I have no trust in this.
For a very long time (decades?) VMware's Linux build system relied on exactly this ability, and managed to ship and support a binary package on many different Linux distributions using this approach.
Not sure if it still works this way, but I'd say I have trust in it.
- It doesn't allow you to install minor versions of the same library at the same time (thats how you end up with all these 1.so, 2.so)
- So if you want a binary to work across multiple distributions, you can build it on a really old CentOS and if you are lucky it will work, but I have no trust in this.
- Forward-compatibility is not really considered, where you update your libraries and your existing app becomes more powerful.
- The dynamic linker doesn't allow you to load two different library versions at the same time (without a lot of contortions). You would think you could do `dlopen` and `dlsym` on two different `.so` files, and then just have separate function pointers to each version's functions. But the linker loves to load all the symbols into a global namespace for some reason.
I'm sure there are ways to overcome all of this, but I feel too young to understand how it got this way and to old to learn it propertly :-P. If I were to design a new system today, I'd probably do something like .NET's global assembly cache: Just dump all library versions in /lib, and have the linker pick the best one at runtime. And make it easy to detect and react to a missing lib at runtime.