May 3, 2015

Some COM for you - Chapter 2

In which Tigger COM is unbounced


...what they should do is take Tigger on a long walk to somewhere where he won't know where he is, and then leave him there, and when they go back to find him the next morning 

he will have learnt his lesson and been unbounced.
A. A. Milne




We ended chapter 1 having working Delphi - .Net interop via COM, but with one unfortunate issue - a need to register a library on every machine with absolute path. A solution for this would be to use Registration-free COM also known as Side-by-Side COM. This chapter is how to make it work, and though it worked in the end, the whole process is a bit thorny and winding and fragile and... well, it's possible to make it work still.

Rube Goldberg coffee machine by Dina Belenko



So, the registration-free COM is about describing the system what .dll's are required to run the app and what types they export. We will be providing same info that is stored in registry when doing regular COM registration, but via other means. The process is described in this MSDN post, but there are caveats that costed me a couple of sleepless nights.

So let's get going. It all starts with manifests. We need two manifests. First one is to describe our assembly and another - to make our app aware of the assembly. The notion of "assembly" in COM sense is different from .Net, but for us let's just think of our dll to be an "assembly". Manifests for the purpose of side-by-side are (poorly and wrongly) documented on MSDN.

So we need to create a manifest for our ComDllNet.dll .  To make sure that we get the assembly right, we may use mt.exe tool from VS developer command prompt:
mt.exe -managedassemblyname:ComDllNet.dll -nodependency -out:ComDllNet.manifest

Which would give us a ComDllNet.manifest file which (after pretty printing) looks like this:
<?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"/>
 </assembly>

What we got is a description of our assembly (I've used a placeholder for the publicKeyToken). But one thing is missing, and this is a description of ICallbackHandler interface. The thing is, we need to explain to Side-by-Side system that it has to create a stub for this interface based on the description in the .tlb file. More on this here, and also in this SO answer that helped me to put all things together finally. So the manifest would be like this:

<?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"/>
 <file name="ComDllNet.tlb">
  <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>

Note that:
  • we had to add another file tag describing the .tlb file and explain that it has a tlb inside;
  • reference the tlb in the separate comInterfaceExternalProxyStub tag and also specify a proxyStubClsid32 with GUID of IUnknown COM interface.
It's sad that the tool is not clever enough to do this for us.


Now, we need another manifest - for the client application to describe that we actually want to use the Side-by-Side COM assembly, and where to get it. If it was for a regular application, we'll just need to place ComApp.exe.manifest near to our ComApp.exe and it should have contained the following part:

<dependency>
 <dependentAssembly>
  <assemblyIdentity
     name="ComDllNet"
     version="1.0.0.0"
     publicKeyToken="xxxxxxxxxxxxxxxx"
     processorArchitecture="x86"/>
 </dependentAssembly>
</dependency>

But in fact, our Delphi app has a manifest on its own, and it's not OK to have two manifests for single exe. The default manifest if provided by Delphi, it contains guides on using UI themes, and it's embedded into the app. So for now we have to do the following.

First we need to disable default manifest, as we have no control over it's contest. So go to Project > Options > Application, and uncheck Enable runtime themes. Then, we'll create a ComApp.exe.manifest file of the following contents:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity
    type="win32"
    name="CodeGear RAD Studio"
    version="14.0.3615.26342" 
    processorArchitecture="*"/>
  <dependency>
    <dependentAssembly>
      <assemblyIdentity
        type="win32"
        name="Microsoft.Windows.Common-Controls"
        version="6.0.0.0"
        publicKeyToken="6595b64144ccf1df"
        language="*"
        processorArchitecture="*"/>
    </dependentAssembly>
  </dependency>
  <dependency>
   <dependentAssembly>
    <assemblyIdentity
        name="ComDllNet"
        version="1.0.0.0"
        publicKeyToken="xxxxxxxxxxxxxxxx"
        processorArchitecture="x86"/>
   </dependentAssembly>
  </dependency>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel
          level="asInvoker"
          uiAccess="false"/>
        </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

The contents is the same as in the default manifest, provided by Delphi 2010, except for a dependency tag referencing our assembly. Note that assemblyIdentity tag contents should be exactly the same as in the declaring manifest. Also note that in this comprehensive MSDN post written that it is necessary to give assembly a different name from the dll, e.g. "ComDllNet.X", but this was not working for me.

Now the magic is to embed the manifest back into the app. We need to create a ComApp.exe.manifest.rc resource script file of the following contents:

#define RT_MANIFEST 24 
#define APP_MANIFEST 1

APP_MANIFEST RT_MANIFEST ComAppDelphi.exe.manifest

And add it to the Delphi project via Project > Add To Project. You should get the following line in the project file:

{$R 'ComAppDelphi.exe.manifest.res' 'ComAppDelphi.exe.manifest.rc'}

When you build the project, there should be a ComApp.exe.manifest.res file which is a compiled resource containing our manifest (could be checked with a text editor). And it should get embedded into the application's .exe file.

Well, that's mostly it. Now you may unregister the .dll (and don't forget to specify the .tlb so it would be unregistered too, or you'll get a lot of weird results!):

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe c:\Dev\Projects\ComTest\ComDllNet.dll /tlb: c:\Dev\Projects\ComTest\ComDllNet.tlb /unregister

Don't forget that you need administrative rights to run this properly.


So, as this point you should be able to run the ComApp.exe and achieve exactly the same results as in part one! Your app now consists of ComApp.exe, ComDllNet.dll, ComDllNet.tlb and ComDllNet.manifest, which should all be in the same directory.

Still for me it took a lot of trial and error to have it working, so probably you'll face some issues too. Here are some tips that could be useful for troubleshooting issues:

  • Don't forget to unregister the .dll and .tlb! You may check that the app is working while .dll/.tlb are registered, but in the end you are not going to have it on target machine. Anyway it's good to check the app later on another machine where you never had anything registered.
  • It took me a day of struggling with weird issues to find out that SxS system seem to cache the manifests! So when you change the contents of the ComDllNet.manifest you may have to do the magic dance to make the system notice it. For me it was clearing the file completely, and the rebuilding and running host app until it shows error that the side-by-side configuration is wrong (which we assured by clearing the file). And only then I put new contents inside manifest. Not too robust, but this way I was sure it's not cached. Note that it took me 2-3 times of running the app with empty manifest until the system finally notices the change.
  • Check Event Log. When you get an error popup, it's usually have no useful information, but you can get more from Application Event Log looking for SideBySide source. Most of the time though, it would contain "Use sxstrace.exe for more details" message. And you really should do so. You will get a complete view of how Side-by-Syde system tries to find and load the assemblies. A bit of details on sxstrace usage is here.
  • Use Assembly Binding Log Viewer aka Fuslogvw.exe. This tool shows the log of .Net assemblies loading, so it's useful when you are past the SxS side and in the .Net realm. You'll need administrative rights to turn logging on/off.

Also here is a conclusion on useful links that I found while looking for solutions:

Closing thoughts. It works! Though troublesome and really hard to do right on first time, it does work! And you don't have to worry about registering the .dll on every machine you want your app to run, or worry about dll hell.

Still you need to distribute 3 files (.dll, .tlb and .manifest) instead of a single dll. It should be possible to embed .manifest and .tlb files into the dll (see this and this), read Chapter 3 if you want a recipe. It also should be possible to put .dll file into some subfolder (see this and this).

No comments:

Post a Comment