May 9, 2015

Some COM for you - Chapter 3

We finished Chapter 2 having registration-free COM working, but having 3 files (.dll, .tlb and .manifest) instead of single .dll. Now, it's possible to embed both .tlb and .manifest into .dll.

Here's the recipe.



First, we will need to modify the manifest, because now type library would be inside the .dll file. So no separate file for .tlb in the manifest:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
 <assemblyIdentity 
   name="ComDllNet"
   version="1.0.0.0"
   publicKeyToken="xxxxxxxxxxxxxxxx"
   processorArchitecture="x86"/>
 <clrClass
   clsid="{XXXXXXXXX-XXXX-XXXX-XXXX-000000000004}"
   progid="ComDllNet.ComServer"
   threadingModel="Both"
   name="ComDllNet.ComServer"
   runtimeVersion="v4.0.30319"/>
 <file name="ComDllNet.dll"
   hashalg="SHA1">
   <typelib tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-000000000001}"
     version="1.0"
     helpdir="."
     flags=""/>
   </file>
 <comInterfaceExternalProxyStub 
   iid="{XXXXXXXX-XXXX-XXXX-XXXX-000000000002}"
   name="ICallbackHandler"
   tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-000000000001}"
   proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"
 />
</assembly>

Now the files are being embedded as Win32 resource (.res file, don't confuse with .Net .resource file). So we must prepare the .res file. Res files are created with Resource Compiler tool which compiles .rc file into .res. Our ComNetDll.rc file would be of the following contents:

1 TYPELIB ComDllNet.tlb
1 24 ComDllNet.manifest

Read more about .rc files here. So you see, we just referencing the .tlb and .manifest files with a bunchof magic numbers. Note that there's no resource definition macro defined for manifest by MS (which is strange beyond reasons), but there's a constant - 24 (not 42 though). Now, compile ComNetDll.rc with rc.exe using developer command prompt:

rc.exe ComNetDll.rc

Now, we get ComNetDll.res file and what remains is to specify it in the dll project settings (Project > Propertis > Application > Resources > Resource file) and rebuild the dll. As a result you should get a .dll with .tlb and .manifest embedded (which you may check viewing .dll file as text and looking for familiar symbols). And this way you may distribute a single file instead of three.


While it's already cool, there's more to it. Keeping things as-is means that you would have to manually rebuild tlb file (via registering/unregistering .dll) each time you change something in COM-visible part of your .dll interface, and then manually recompile the .res file. And before creating a new tlb, you'll have to switch your project back to not using .res file, or you'll get old tlb extracted from your .dll instead of a new one being generated. And then switch back to using .res file...

Really, a lot of things to do in right order, and if you forget to do something, you'll end up having all kinds of weird behaviours or runtime exceptions in your host app, and have hard time figuring that it was your .dll to blame (took me a night to figure out that I forgot to update tlb after adding a new method to public interface).

So the right way would be to automate this. I found a solution using the PostBuild event of the project. Here it is. The proper order to do things would be:


  1. Build a dll without anything embedded - just your regular project build with Resources setting of the project set to "Icon and manifest".
  2. Make a manifest - this step I don't plan to automate, as I see no easy way to generate full manifest including all the tlb references to right files. Anyway, you actually need to update your manifest only after adding new COM-visible classes, which should be rather rare. See Chapter 2 for instructions on using mt.exe tool to generate part of a manifest, and manually editing it. So I assume we have a manifest in the project root folder.
  3. Generate a .tlb file - in contrast to manifest, tlb should be updated every time you change something in the COM-visible interface of your dll, i.e. when you add a parameter to a function. There's a nice way to do this using a Type Library Exporter (tlbexp.exe) tool.
  4. Compile .rc to .res - so that new tlb get into it. Just use the Resource Compiler (rc.exe) tool as above.
  5. Build .dll again with .res file embedded - this is the most tricky thing. We have to re-build the project with our .res file. The most appropriate thing to use here seem to be MSBuild - the build system that is used by VS to build the project in the first place. We just need to specify the .res file and we want to prevent our post-build event to run again, or it would end spawning build processes unless your PC runs out of space and time. Nice thing is that it's possible to override parts of the project file known as "properties".
  6. Build Pascal wrapper for the .tlb (optional) - if you still want to use the library with Delphi (like in Chapters 1 and 2) you'll need to generate a _TLB.PAS file, and you cannot use Import Component IDE menu because the library is not registered anymore (and opening .tlb or .dll in the dialog does not work for me). What you may use is the tlibimp tool from the Delphi distribution. Using the following set of the options : -P  -Ha- -Hr- -Hs- -R- -D  I was able to get the same file as the one generated via IDE.

So let's put all of this together. I got the following post-build event:
"$(TargetFrameworkSDKToolsDirectory)TlbExp.exe"   "$(ProjectDir)$(OutDir)$(TargetFileName)" /out:"$(ProjectDir)$(OutDir)$(TargetName).tlb"

copy /B /Y "$(ProjectDir)$(OutDir)$(TargetFileName)" "$(ProjectDir)$(OutDir)$(TargetName).no_embedding$(TargetExt)"

copy /B /Y "$(ProjectDir)$(TargetName).manifest" "$(ProjectDir)$(OutDir)$(TargetName).manifest"

copy /B /Y "$(ProjectDir)$(TargetName).rc" "$(ProjectDir)$(OutDir)$(TargetName).rc"

rc "$(ProjectDir)$(OutDir)$(TargetName).rc"

"$(MSBuildToolsPath)\MSBuild.exe" /p:Configuration=$(ConfigurationName) /p:Win32Resource="$(ProjectDir)$(OutDir)$(TargetName).res" /p:PostBuildEvent="@echo Done!"  "$(ProjectPath)"

tlibimp -P  -Ha- -Hr- -Hs- -R- -D"$(ProjectDir)$(OutDir)" "$(ProjectDir)$(OutDir)$(TargetName).tlb"

Note the override for Win32Resource and PostBuildEvent properties for MSBuild. The event expects that .manifest and .rc files exist in the project root folder and have same name as the output .dll.

That seem to be all to it! Now you get the .dll file with the .manifest and always actual .tlb embedded automatically!




No comments:

Post a Comment