TL;DR: It’s worth not just detecting, but also trying out whether it actually works.
If you ship software on Windows written in C++ and compiled with the MSVC toolchain, then you probably heard about the so-called “Visual Studio C++ Redistributable”. Chances are that you link your binaries against the dynamic runtime DLLs, which means that your application has a runtime dependency of (at least some of the) DLLs contained in this redistributable package.
When your product is installed, you (or the installer framework you use) have to make sure that the appropriate version of the redistributable is also installed, or your program will fail to start. In this post, I’ll reason about why you shouldn’t settle for merely detecting whether this dependency is installed, with two real-world examples.
If a program can’t start because a static DLL dependency cannot be loaded (maybe it’s missing, or damaged etc.), you get a helpful error dialog stating the name of the affected DLL, which gives you a decent pointer on fixing the issue. Sometimes, however, the stated error message doesn’t make much sense (for example, it complains about the wrong binary).
I remember a technical support case where one of our users reported that after successfully installing our product, it failed to start because of a DLL that was reported to be missing. The stated DLL was present and intact, so the error made no sense. In cases like this, making the loader emit verbose diagnostic messages comes in handy. The good news is that activating this feature only takes setting a global flag. The bad news is that an actual debugger needs to be attached to the process for these messages to be emitted.
We took action and investigated the issue on the user’s machine. After activating verbose loader logging (the official name of this is enabling “showing loader snaps”) and examining the output, it turned out that in the System32 directory of the user, vcomp140.dll, the OpenMP runtime DLL of the Visual Studio 2015 redistributable, was a 32-bit DLL (instead of 64-bit, as it should be in the System32 folder).
We couldn’t find out how it got there (and to be honest, we didn’t want to), but it was easy to fix: all we had to do is run the redistributable’s installer in repair mode. After that, our program could start with no issues1.
A few years ago, Microsoft made the C runtime a component of the operating system (sort of2). Since this decision was made before the final release of Windows 10, this is installed by default on Windows 10 systems. On earlier versions however, this component (called ucrtbase.dll) is provided through Windows Update. If you compile your application with Visual Studio 2015 or later, your binaries will have a dependency on ucrtbase.dll. This means that in order to run these programs on Windows versions earlier than 10, the update package containing this new Universal C runtime must be installed first. The redistributable installers have this update bundled, so if your system does not have ucrtbase.dll yet, the Visual C++ runtime installer will install the required Windows Update first.
And here comes the catch: the update package has prerequisites. For example, on Windows 7 you need at least Service Pack 1, and on Windows 8.1, the “April 2014 update rollup” is required. What happens when you try to install the redistributable onto a system that does not have the prerequisite updates? In case of the Visual Studio 2017 redistributable, you get a generic error message with an error code, stating that something went wrong, and installation fails. With version 2015, you also get a generic error, installation also fails, seemingly.
Seemingly, because even though the installation process was interrupted with an error:
- All runtime DLLs were copied to their destination (with the exception of ucrtbase.dll, of course)
- The installation got registered, so it shows up in Control Panel’s Programs and Features list
If your installer tries to install the VS 2015 redistributable on an un-updated machine, it will fail and return an error code. That’s nice because you can detect that something went wrong, at least. But if someone else tried installing it before you, it’s very well possible that you are up against a corrupt installation of the runtime, while the system happily reports that it’s all well and installed.
The solution to this problem is quite trivial: even if the system reports that the redistributable is installed, don’t trust that blindly. Be skeptical, and actually try it out if it works. How do you do that? With a carefully crafted, small test program:
- Create a DLL that has static runtime dependencies of all redistributable DLLs. An exported function that calls standard functions will do fine. Make sure you depend on all the required DLLs with Dependency Walker.
- Create an executable that links to the static version of the runtime (by doing this, you make sure that this binary has no dependencies on redist DLLs). All this program needs to do is load the DLL described in the previous point with LoadLibrary, and return 0 as its exit code, if it succeeded.
The exe will start regardless of any redistributable installations, since the CRT was linked into it statically. If it can load our tester DLL, it means the loader could find and load all VC redistributable DLLs. If it can’t, it means your program you were going to try and install won’t work either.
The company I work for has been using a small utility like this for quite a long time, and it has proven very useful over the years.
For some people, this will definitely seem like an overkill solution. Whether all this extra effort is worth it is really up to the number of users you have, and the variety of their environments. Either way, if you google the generic error message that you get when an application fails to start because of a missing DLL, the majority of results are about VC redist DLLs (which sort of gives an indication).
1 You might wonder why the user didn’t have any problems with other programs (surely, there are a lot of applications out there using this particular version of the runtime). The answer is that only this one DLL was “corrupted”, so only applications relying on OpenMP were affected.
2 They actually split the CRT into two parts: one that is stable (ucrtbase.dll), and one that is not (vcruntime[v].dll). Naturally, the stable part is the one that’s now part of the OS. You can read about this in more detail in this article.