Don't you have to compile separately for each `msvcrt` environment, as I thought they aren't binary compatible? And would a non-C99 msvcrt necessarily have an `snprintf()` implementation in its libc-equivalent dll?
You can call code compiled with one msvcrXX from code compiled with a different msvcrXX, provided that you don't try passing things from one to the other (that is, no passing a pointer to a FILE structure, or even a file descriptor since the file descriptor table is on the msvcrXX instead of the kernel), and always free or realocate memory using the same msvcrXX (that is, don't allocate memory and expect your caller to call free() on it, always provide a custom deallocation function for your objects).
This is possible because, unlike on Linux where function names are global, on Windows function names are scoped to the DLL, so you can have MSVCR71.DLL and MSVCR81.DLL loaded at the same time in the same process and they won't interfere with each other.
OK, but isn't the point of building a program that links to (say) MSVCR71.DLL that you're expecting to run it in an environment where (say) MSVCR81.DLL isn't available?
I don't see how that fixes the problem of possibly not having an snprintf() implementation on a system that doesn't have a C99-compatible MSVC runtime environment.
Did I miss an implication of your comment somehow?
If you compile your program against some MSVCRT then it's your job to make sure that MSVCRT is available on the machine where your program is installed, by delegating to its installer.
All supported MSVCRTs are installable on all supported Windows SKUs.
Yes, that's the thing I always forget, the way Windows deals with multiple incompatible versions of msvcrt is that every application ships its own copy of libc, and hopefully the installer is well-written enough to only copy it into place if it's newer than the newest release of the same major version that's already there, lest a random app re-introduces a bunch of security issues that should have been closed by the last security update for every other application that uses the same msvcrt.
...and by "forget", I mean "block out due to trauma, because surely it can't be that stupid".
Linker scripts change whether or not symbols are added to a global symbol table for subsequent requests (i.e. "exported"). Though, you don't even need a linker script to effect visibility as both GCC and clang provide a visibility function attribute, and you can change the default visibility through a simple compiler command switch.
dlopen permits you to control whether exported (externally visible) functions in a module become available to satisfy link dependencies in the application, such as subsequent module loads. See the dlopen flags RTLD_GLOBAL and RTLD_LOCAL.
dlmopen is for controlling the visibility of shared library dependencies pulled in by dlopen'd modules, whether RTLD_GLOBAL or RTLD_LOCAL, which only effect the immediate symbols in the module and not symbols from automatically loaded shared library dependencies. If you link the main application with OpenSSL (-lssl -lcrypto), or a prior module you dlopen'd pulled in OpenSSL as a dependency, then those OpenSSL symbols become available to satisfy requirements for subsequent dlopen'd modules. dlmopen allows you to create an entirely different symbol namespace for a module or modules, where symbols dependencies are only ever satisfied from that namespace, and exported (global) symbols, whether pulled in by dlopen or transitively via a shared library, are never visible outside that namespace.
None of these options directly map to the behavior of DLLs. DLLs fundamentally use different semantics, AFAIU. The closest behavior to DLLs might be DT_RUNPATH + dlmopen, but dlmopen use is explicit so not really the same thing. You could use ELF symbol versioning (maybe in combination with DT_SONAME and DT_RUNPATH) to accomplish the same thing as DLLs by effectively renaming all the symbols in a library (e.g. attaching a version component), but there aren't any tools around to help automate that, AFAIK; you'd have to generate linker scripts and it'd be a complex build. Much easier to just static link at that point.
For C, Windows has had a stable CRT (libc in Unix speak) for several years now, since Win10. There are still cross-runtime compatibility concerns with C++, but those shouldn't apply here.
Particularly the MSVC ecosystem is identified in TFA as being a late adopter.
Once built c99 code can run where it wants.