Fast Builds: Incremental Linking and Embedded SxS Manifests
As I've said before, fast builds are crucial for efficient development. But for those of us who use C++ regularly, link times are killer. It's not uncommon to spend minutes linking your compiled objects into a single binary. Incremental linking helps a great deal, but, as you'll see, incremental linking has become a lot harder in the last few versions of Visual Studio...
Linking an EXE or DLL is a very expensive operation -- it's roughly O(N) where N is the amount of code being linked. Worse, several optimizing linkers defer code generation to link time, exacerbating the problem! When you're trying to practice TDD, even a couple seconds in your red-green-refactor iteration loop is brutal. And it's not uncommon for large projects to spend minutes linking.
Luckily, Visual C++ supports an /INCREMENTAL flag, instructing relinks to modify the DLL or EXE in-place, reducing link time to O(changed code) rather than O(all code). In the olden days of Visual C++ 6, all you had to do was enable /INCREMENTAL, and bam, fast builds.
These days, it's not so simple. Let's take an excursion into how modern Windows finds DLL dependencies...
Side-by-Side (SxS) Manifests
Let's say you're writing a DLL foo.dll
that depends on the CRT by using, say, printf
or std::string
. When you link foo.dll
, the linker will also produce foo.dll.manifest
. Windows XP and Vista use .manifest files to load the correct CRT version. (This prevents DLL hell: two programs can depend on different versions of the same DLL.)
Since remembering to carry around .manifest files is annoying and error-prone, Microsoft and others recommend that you embed them into your EXE or DLL as a resource:
mt.exe -manifest foo.dll.manifest -outputresource:foo.dll;2
Convenient, but it modifies the DLL in place, breaking incremental links! This is a known problem, and the "solutions" others suggest are INSANE. My favorite is the 300-line makefile with a note from the author "[If this does not work], please let me know ASAP. I will try fixing it for you." Why doesn't Visual Studio just provide an /EMBEDMANIFESTRESOURCE flag that would automatically solve the problem?!
I just want incremental linking and embedded manifests. Is that so much to ask? I tried a bunch of approaches. Most didn't work. I'll show them, and then give my current (working) approach. If you don't care about the sordid journey, skip to the end.
What Didn't Work
- Not embedding manifests at all.
What went wrong: I could never figure out the rules where by manifest dependencies are discovered. If python.exe depends on the release CRT and your module DLL depends on the debug CRT, and they live in different directories (??), loading the module DLL would fail. Gave up.
- Linking a temporary file (foo.pre.dll), making a copy (foo.pre.dll -> foo.dll), and embedding foo.pre.dll.manifest into foo.dll with mt.exe.
What went wrong: As far as I can tell, mt.exe is a terrible piece of code. In procmon I've watched it close file handles it didn't open, causing permissions violations down the line. (?!) Sometimes it silently corrupts your EXEs and DLLs too. This may be a known weakness in UpdateResource. Yay! (Thanks to Kevin Gadd; he was instrumental in diagnosing these bugs.) mt.exe may or may not be fixed in recent Visual Studios. Either way, I'm convinced mt.exe has caused us several intermittent build failures in the past. Avoiding it is a good thing.
- Linking to a temporary file (foo.pre.dll), generating a resource script (foo.pre.rc) from (foo.pre.dll.manifest), compiling said resource script (foo.pre.res), and including the compiled resource into the final link (foo.dll).
What went wrong: This approach is reliable but slow. Linking each DLL and EXE twice, even if both links are incremental, is often slower than just doing a full link to begin with.
- Linking foo.dll with foo.dll.manifest (via a resource script, as above) if it exists. If foo.dll.manifest changed as a result of the link, relink.
I didn't actually try this one because non-DAG builds scare me. I like the simplicity and reliability of the "inputs -> command -> outputs" build model. It's weird if foo.dll.manifest is an input and an output of the link. Yes, technically, that's how incremental linking works at all, but the non-DAG machinery is hidden in link.exe. From SCons's perspective, it's still a DAG.
Finally, a working solution:
For every build configuration {debug,release} and dependency {CRT,MFC,...}, link a tiny program to generate said dependency manifest. Compile manifest into a resource script (.rc -> .res) and link the compiled manifest resources into your other DLLs and EXEs.
This approach has several advantages:
- These pre-generated manifest resources are created once and reused in future builds, with no impact to build time.
- The build is a DAG.
- We avoid letting mt.exe wreak havoc on our build by sidestepping it entirely.
I can think of one disadvantage - you need to know up-front on which SxS DLLs you depend. For most programs, the CRT is the only one. And hopefully understanding your dependencies isn't a bad thing, though. ;)
After several evenings of investigation, we're back to the same link times we had with Visual C++ 6! Yay!
The Code
If you care, here's our SCons implementation of embedded manifests:
# manifest_resource(env, is_dll) returns a manifest resource suitable for inclusion into # the sources list of a Program or SharedLibrary. manifest_resources = {} def manifest_resource(env, is_dll): if is_dll: resource_type = 2 #define ISOLATIONAWARE_MANIFEST_RESOURCE_ID 2 else: resource_type = 1 #define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 is_debug = env['DEBUG'] # could use a 'build_config' key if we had more than debug/release del env def build_manifest_resource(): if is_debug: env = baseEnv.Clone(tools=[Debug]) else: env = baseEnv.Clone(tools=[Release]) env['LINKFLAGS'].remove('/MANIFEST:NO') if is_dll: linker = env.SharedLibrary target_name = 'crt_manifest.dll' source = env.File('#/MSVC/crt_manifest_dll.cpp') else: linker = env.Program target_name = 'crt_manifest.exe' source = env.File('#/MSVC/crt_manifest_exe.cpp') env['OUTPUT_PATH'] = '#/${BUILDDIR}/${IMVU_BUILDDIR_NAME}/%s' % (target_name,) obj = env.SharedObject('${OUTPUT_PATH}.obj', source) result = linker([env.File('${OUTPUT_PATH}'), '${OUTPUT_PATH}.manifest'], obj) manifest = result[1] def genrc(env, target, source): [target] = target [source] = source # 24 = RT_MANIFEST file(target.abspath, 'w').write('%d 24 "%s"' % (resource_type, source.abspath,)) rc = env.Command('${OUTPUT_PATH}.rc', manifest, genrc) res = env.RES('${OUTPUT_PATH}.res', rc) env.Depends(res, manifest) return res key = (is_debug, resource_type) try: return manifest_resources[key] except KeyError: res = build_manifest_resource() manifest_resources[key] = res return res
Really useful info, thanks.
Surely it wouldn't have taken a Microsoft engineer more than half a day to add support for automated embedding of manifests via resource scripts?
Especially as the situation as is means most people just aren't getting incremental linking at all!
Really useful info indeed. We ended up using a slightly different approach. We essentially replace the default SCons builder to add all of these extra steps to support incremental linking. Here is how we do it:
envDebug = ... whatever environment you have ...
#------------------------------------------------------------ # Link with debugging informations
# envDebug.AppendUnique(LINKFLAGS=['/DEBUG']) #------------------------------------------------------------ # Dynamically link with the debugging CRT # envDebug.AppendUnique(CCFLAGS=['/MDd']) #------------------------------------------------------------ # Disable optimizations # envDebug.AppendUnique(CCFLAGS=['/Od']) #------------------------------------------------------------ # Produce one .PDB file per .OBJ when compiling, then merge them when linking. # Doing this enables parallel builds to work properly (the -j parameter). # See: http://www.scons.org/doc/HTML/scons-man.html section CCPDBFLAGS # envDebug['CCPDBFLAGS'] = '/Zi /Fd${TARGET}.pdb' envDebug['PDB']='${TARGET.base}.pdb' #------------------------------------------------------------ # Link incrementally. Produces larger (and possibly corrupted files, # but takes less time to build. Not recommended for final build. # envDebug.AppendUnique(LINKFLAGS=['/INCREMENTAL']) #------------------------------------------------------------ # Hacks to allow incremental linking on Microsoft Visual C++. # # 1 - Don't generate a .manifest file, we're doing that below # envDebug.AppendUnique(LINKFLAGS=['/MANIFEST:NO']) # # 2 - Override the SharedLibrary builder to add some extra steps # def SharedLibraryIncrementallyLinked(env, library, sources, *args): # # Hack 1: Embed a manifest while linking # # Since we can't embed the .manifest file *AFTER linking, because it # would modify the binary and prevent subsequent incremental linking, # we must embed it WHILE linking. We achieve that by generating a # manifest file from a dummy program created with the current # environment. This manifest file is then added to a resource, which # is compiled into an object file and linked into the final binary. # subBuild = env.Clone() subBuild['WINDOWSINSERTMANIFEST'] = True subBuild.AppendUnique(LINKFLAGS='/MANIFEST') subBuild.AppendUnique(LINKFLAGS='/INCREMENTAL:NO') if '/MANIFEST:NO' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/MANIFEST:NO') if '/INCREMENTAL' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/INCREMENTAL') if '/DEBUG' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/DEBUG') del subBuild['PDB'] def createDummySourceFile(env, target, source): file(target[0].abspath, 'w').write("int main(int, int *) { return 0; }\n") dummyName = library[0] + '_dummy_for_manifest' dummySourceFile = subBuild.Command(dummyName + '.cpp', None, createDummySourceFile) dummyProgram = subBuild.ProgramOriginal(dummyName + '.exe', dummySourceFile) dummyManifest = dummyProgram[1] def createManifestResourceFile(env, target, source): file(target[0].abspath, 'w').write('2 24 "%s"' % source[0].abspath.replace('\','\\')) manifestResourceFile = subBuild.Command(dummyName + '.rc', dummyManifest, createManifestResourceFile) manifestResource = subBuild.RES(dummyName + '.res', manifestResourceFile) # # Hack 2: Precious binary # # By default, SCons will delete the files before re-building them. # This prevents incremental linking from working because it relies # on timestamps. Therefore, we must prevent SCons from deleting the # the build products. This is achieved by making them as "Precious". # library = env.SharedLibraryOriginal(library, [sources, manifestResource], *args) env.Precious(library) return library envDebug['BUILDERS']['SharedLibraryOriginal'] = envDebug['BUILDERS']['SharedLibrary'] envDebug['BUILDERS']['SharedLibrary'] = SharedLibraryIncrementallyLinked # # 3 - Override the Program builder to add some extra steps # def ProgramIncrementallyLinked(env, program, sources, *args): # # Hack 1: Embed a manifest while linking # # Since we can't embed the .manifest file *AFTER linking, because it # would modify the binary and prevent subsequent incremental linking, # we must embed it WHILE linking. We achieve that by generating a # manifest file from a dummy program created with the current # environment. This manifest file is then added to a resource, which # is compiled into an object file and linked into the final binary. # subBuild = env.Clone() subBuild['WINDOWSINSERTMANIFEST'] = True subBuild.AppendUnique(LINKFLAGS='/MANIFEST') subBuild.AppendUnique(LINKFLAGS='/INCREMENTAL:NO') if '/MANIFEST:NO' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/MANIFEST:NO') if '/INCREMENTAL' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/INCREMENTAL') if '/DEBUG' in subBuild['LINKFLAGS']: subBuild['LINKFLAGS'].remove('/DEBUG') del subBuild['CCPDBFLAGS'] del subBuild['PDB'] def createDummySourceFile(env, target, source): file(target[0].abspath, 'w').write("int main(int, int *) { return 0; }\n") dummyName = program[0] + '_dummy_for_manifest' dummySourceFile = subBuild.Command(dummyName + '.cpp', None, createDummySourceFile) dummyProgram = subBuild.ProgramOriginal(dummyName + '.exe', dummySourceFile) dummyManifest = dummyProgram[1] def createManifestResourceFile(env, target, source): file(target[0].abspath, 'w').write('1 24 "%s"' % source[0].abspath.replace('\','\\')) manifestResourceFile = subBuild.Command(dummyName + '.rc', dummyManifest, createManifestResourceFile) manifestResource = subBuild.RES(dummyName + '.res', manifestResourceFile) # # Hack 2: Precious binary # # By default, SCons will delete the files before re-building them. # This prevents incremental linking from working because it relies # on timestamps. Therefore, we must prevent SCons from deleting the # the build products. This is achieved by making them as "Precious". # program = env.ProgramOriginal(program, [sources, manifestResource], *args) env.Precious(program) return program envDebug['BUILDERS']['ProgramOriginal'] = envDebug['BUILDERS']['Program'] envDebug['BUILDERS']['Program'] = ProgramIncrementallyLinked