Containers and DLL Hell

Back in the 1990s, shared libraries were all the rage. Instead of having to ship a 20 MB *.exe file to your customer in various floppy disks, you could cut some code out, put it in a set of *.dll files, and reuse that code across all your products. Every vendor would then install lots of DLL files in your system, and they would be reused by other apps from the same vendor.

Why did they do that? Well, storage space was expensive back then. An internal 1 GB hard disk in 1995 costed me around a thousand Swiss Francs, and it filled fast with all those apps (well, I also had Windows 3.1 and OS/2 Warp installed simultaneously.) So reusing code in DLLs appeared, at least at first sight, as a valuable mechanism to reduce bloat for software vendors, and to avoid repeating themselves.

The truth is that those DLLs multiplied themselves like bacteria, and led to a concept that made headlines back in 1995: DLL Hell. The concept was so pervasive and problematic that it got its own Wikipedia entry.

Talk about being a celebrity.

Even Microsoft couldn’t cope with the DLL explosion; apps would crash at runtime because they would load DLLs with the same name, but with different versions of the same functions inside. It was really a mess.

The most egregious example of this mess was Visual Basic itself, and its suite of VBRUN300.DLL libraries, of which it seemed there were gazillion versions in shareware collections everywhere. Which one to use? Well, you’re out of luck; try them one by one until your game runs doesn’t crash anymore.

The best way to prevent DLL Hell was (still is) to link binaries with libraries at build time. But of course, if your storage is expensive, well, it’s not the best way to do things, because the final executable will be more bulky at the end. But, look ma! No DLL loading at runtime! Static binaries are the best.

But this is only valid for compiled languages, really. If your app is written in PHP, Python, Ruby, Perl, Lua, or (more recently) JavaScript, well, you’ll need to install a few gigabytes of packages and runtimes for them to run. README is your friend, and then npm install or pip install or gem install until everything works.

We’re in 2022 now, almost 30 years later, and statically built binaries have won, and even for scripting languages! They are called containers now; Docker containers, pods, what have you, and they encapsulate not only the final executable and its libraries, but also any other piece of runtime code they might need. Like the whole .NET framework, or the PHP runtime, or a complete version of Ruby and Ruby on Rails including all the gems your app needs to get things done.

It does not matter anymore if your binary is actually statically linked, though; apps written in Ruby, Python, PHP, JavaScript, Lua and other similar languages are just interpreted on the fly by the runtime installed within the same container.

The downside is that with those languages your final container image could easily stretch into the gigabytes… not very convenient if you want to run many copies of those containers in the same load-balanced Kubernetes deployment. Oops.

This all means that compiled languages are making a nice comeback right now; Go, Rust, D, C++, .NET, Crystal, C, they can all be used to generate a small self-contained executable, and with it, a very small final container image.

Specify in your Dockerfile a base image FROM scratch, copy your binary, and your container image is now just 15 MB big. Push it, share it, pull it, and reuse it. And apparently .NET 7 (the latest version) includes a new AOT feature that makes really small native binaries. Finally. Oh, and you can even dotnet publish directly as a container.

Talk about convenient. Make small container images, people.