How to provide a private Side by Side manifest that correctly locates a .NET Dll as COM Provider?

19

I'm researching about the configuration of a private registration free WinSxS with the plain provision of assembly manifest files, to stitch Delphi executables (COM clients) and .NET (C#) COM visible DLLs together at deployment and runtime.

I already studied the documentation available at MSDN "Interoperating with Unmanaged Code", the sections about "COM Callable Wrapper" and "How to: Configure .NET Framework-Based COM Components for Registration-Free Activation" in particular.

After even more than one week of research and being (re-)directed in cycles of insufficient documentation, I decided to place my 1st question ever here.

The planned deployment structure looks as follows:

./install-root
├───ProgramSuite1
│   ├───bin
│   │       DelphiNativeCOMClient1.exe
│   │       DelphiNativeCOMClient1.exe.config
│   │       DelphiNativeCOMClient2.exe
│   │       DelphiNativeCOMClient2.exe.config
│   |       ...
│   │
│   └───data
│           ...
├───ProgramSuite2
│   ├───bin
│   │       DelphiNativeCOMClient3.exe
│   │       DelphiNativeCOMClient3.exe.config
│   │       DelphiNativeCOMClient4.exe
│   │       DelphiNativeCOMClient4.exe.config
│   |       ...
│   │
│   └───data
│           ...
└───SharedLibs
    ├───MyCompany.Libs.Set1
    │       MyCompany.Libs.Set1.manifest
    │       SomeManagedCOMServerA.dll
    │       SomeNativeCOMServerB.dll
    │       SomeNativeCOMServerC.dll
    │
    └───MyCompany.Libs.Set2
            MyCompany.Libs.Set2.manifest
            SomeManagedCOMServerB.dll
            SomeNativeCOMServerX.dll
            SomeManagedCOMServerA.dll

Here's a short sketch about the implementation of the implementation of the Delphi native executables and the C# .NET COM server DLLs (I left out the examples for the native COM Servers, since this stuff already works well and is out of question).
I mainly followed what was provided at "Registration-Free Activation of COM Components: A Walkthrough". The main difference is that I'm utilizing Delphi rather than C, C++ or old VB as a native client.

TestDllConsoleApp.exe

TestDllConsoleApp.dpr

program TestDllConsoleApp;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  DllTests.Common,
  WinApi.ActiveX,
  WinApi.Windows,
  // These were generated using the tlbimplib tool
  CSharpCOMDll_TLB in 'CSharpCOMDll_TLB.pas',
  mscorlib_TLB in 'mscorlib_TLB.pas';

var
    comInterface1 : ICOMInterface1;
    comInterface2 : ICOMInterface2;
    intf1CoClass : _COMImplClass1; 
    intf2CoClass : _COMImplClass2;
    res : HRESULT;
    coInitializeRes : integer;
begin
    //Initialize COM
    coInitializeRes := CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
    if (coInitializeRes <> S_OK) and (coInitializeRes <> S_FALSE) then begin
        System.ExitCode := 1;
        Exit(); // GUARD
    end;
    try
        try
            intf1CoClass := CoCOMImplClass1.Create();
            res := intf1CoClass.QueryInterface(IID_ICOMInterface1,comInterface1);
            System.WriteLn(comInterface1.GetModuleName());

            intf2CoClass := CoCOMImplClass2.Create();
            res := intf2CoClass.QueryInterface(IID_ICOMInterface2,comInterface2);
            System.WriteLn(comInterface2.GetModuleName());
        except
        on E: Exception do
            Writeln(E.ClassName, ': ', E.Message);
        end;
    finally
        //Uninitialize COM
        CoUninitialize();
    end;
end.

TestDllConsoleApp.manifest

(embedded with resource ID 1)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> 
    <assemblyIdentity name="MyCompany.Software.Application" processorArchitecture="x86" version="1.0.0.0" type="win32" />
    <description>A native COM client application.</description>
    <asmv3:trustInfo>
        <asmv3:security>
            <asmv3:requestedPrivileges>
                <asmv3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
            </asmv3:requestedPrivileges>
        </asmv3:security>
    </asmv3:trustInfo>
    <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> 
        <application>
            <!-- Windows 10 and Windows Server 2016 --> 
            <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
            <!-- Windows 8.1 and Windows Server 2012 R2 -->
            <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
            <!--  Windows 8 and Windows Server 2012 -->
            <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
            <!-- Windows 7 and Windows Server 2008 R2 -->
            <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
            <!-- Windows Vista and Windows Server 2008 -->
            <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
        </application>
    </compatibility>
    <dependency>
        <dependentAssembly>
            <assemblyIdentity type="win32" name="MyCompany.Libs.Set1" version="1.0.0.0" processorArchitecture="x86" />
        </dependentAssembly>
    </dependency>
</assembly>

TestDllConsoleApp.exe.config

(deployed at the same file location as the executable)

<configuration>  
   <runtime>  
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">  
         <probing privatePath="..\..\SharedLibs"/>  
      </assemblyBinding>  
   </runtime>  
</configuration>  

CSharpCOMDll.dll

(will be deployed at the SharedLibs\MyCompany.Libs.Set1 directory)

Assemblyinfo.cs

#region Using directives
using System;
using System.Reflection;
using System.Runtime.InteropServices;

#endregion
[assembly: AssemblyTitle ("CSharpCOMDll")]
[assembly: AssemblyProduct ("CSharpCOMDll")]
[assembly: AssemblyCopyright ("Copyright 2018")]
[assembly: ComVisible (true)]
[assembly: AssemblyVersion ("1.0.0.0")]
[assembly: Guid ("045d53ab-a9e4-4036-a21b-4fe0cf433065")]

COMImplClass1.cs

// Using namespaces ...
namespace CSharpCOMDll
{
    [Guid("6BDAF8DD-B0CF-4CBE-90F5-EA208D5A2BB0")]
    public interface ICOMInterface1  
    {
        string GetModuleName();
    }
    
    [Guid("4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805")]
    public class COMImplClass1 : ICOMInterface1
    {
        public string GetModuleName() 
        {
            return typeof(COMImplClass1).Module.FullyQualifiedName;
        }
    }
}

COMImplClass2.cs

 // Using namespaces ...
namespace CSharpCOMDll
{

    [Guid("BE69E9C7-1B37-4CA8-A3C1-10BFA9230940")]
    public interface ICOMInterface2  
    {
        string GetModuleName();
    }

    [Guid("067E5980-0C46-49C7-A8F0-E830877FB29C")]
    public class COMImplClass2 : ICOMInterface2
    {
        public string GetModuleName() 
        {
            return typeof(COMImplClass1).Module.FullyQualifiedName;
        }
    }
}

CSharpCOMDll.manifest

(Embedded into the DLL with resource ID 2)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" 
  manifestVersion="1.0">
    <assemblyIdentity
                type="win32"
                processorArchitecture="x86"
                name="CSharpCOMDll"
                version="1.0.0.0" />
    <clrClass
                clsid="{4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}"
                progid="CSharpCOMDll.COMImplClass1"
                threadingModel="Both"
                name="CSharpCOMDll.COMImplClass1" 
                runtimeVersion="v4.0.30319">
    </clrClass>
    <clrClass
                clsid="{067E5980-0C46-49C7-A8F0-E830877FB29C}"
                progid="CSharpCOMDll.COMImplClass2"
                threadingModel="Both"
                name="CSharpCOMDll.COMImplClass2" 
                runtimeVersion="v4.0.30319">
    </clrClass>
</assembly>

And finally the assembly manifest as resolved from the TestDllConsoleApp.manifest dependency entries:

MyCompany.Libs.Set1.manifest

<?xml version="1.0" encoding="UTF-8" standalone="yes"?> 
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> 
    <assemblyIdentity type="win32" name="MyCompany.Libs.Set1" version="1.0.0.0" processorArchitecture="x86" />
    <file name="CSharpCOMDll.dll"> 
        <comClass
            clsid="{4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}"
            threadingModel="Both"
            />
        <comClass
            clsid="{067E5980-0C46-49C7-A8F0-E830877FB29C}"
            threadingModel="Both"
            />
        <comInterfaceProxyStub
            name="ICOMInterface1"
            iid="{6BDAF8DD-B0CF-4CBE-90F5-EA208D5A2BB0}"
            proxyStubClsid32="????"
        />
        <comInterfaceProxyStub
            name="ICOMInterface2"
            iid="{BE69E9C7-1B37-4CA8-A3C1-10BFA9230940}"
            proxyStubClsid32="????"
        />
    </file>
</assembly>

It seems I'm halfway there, but still can't diagnose the actual problem.

There are two variants of failure right now (Please note, that deploying the managed COM server DLLs beside the executable instead of referring to the resolved manifests directory just works fine and as intended):

  1. I completely remove the proxyStubClsid32 attribute in the global manifest:

    • Starting the executable ends up with an exception
      EOleSysError: Error in dll, clsid = {4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}

    • Debugging the exception leads to a HRESULT value

         Error in the DLL (Exception from HRESULT: 0x800401F9 (CO_E_ERRORINDLL))
      
  2. I provide a proxyStubClsid32 attribute in the global manifest:

    • I'm not sure which GUID is actually needed for that attribute.
      As it's mentioned in the documentation it naturally seems to be a corresponding "co class ID" (CLSID) as mentioned in the comClass elements clsid attribute.
    • I alternatively tried to provide the LIBID GUID from the generated ,pas file there.

    Both variants leave me with a pretty useless error traceable with the sxstrace tool1:

     ...
     INFORMATION: Manifestdatei ".\install-root\SharedLibs\MyCompany.Libs.Set1\MyCompany.Libs.Set1.MANIFEST" wird analysiert.
        INFORMATION: Die Manifestsdefinitionsidentität ist ",processorArchitecture="x86",type="win32",version="1.0.0.0"".
     FEHLER: Bei der Generierung des Aktivierungskontextes ist ein Fehler aufgetreten.
     Beendet die Generierung des Aktivierungskontextes.
    

    Note that there wasn't any concise error/info message like

      ... cannot resolve assembly XY ...
    

    before the Activation Context Generation screwed up. There's plenty of references indicating this particular error situation.
    Also the ubiquitous mentions of missing Visual C++ redistributable framework doesn't help here. I'm calling from Delphi, and that's something different.

  3. Another attempt to reference the CSharpCOMDll.dll explicitly (another dependency in the executable manifest), and just place it into the SharedLibs got a successfully created Activation Context, but fails with a slightly different exception than before

    EOleSysError: Cannot find file, clsid = {4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}
    

Does anyone here know how to do what I want straightforward, or what can be done additionally (besides the sxstrace) to diagnose the problem in more depth.

I'm almost sure it must be possible to provide a deployment like this.


TL;DR;

  • Is it even possible to provide a deployment structure like mentioned above, and maintain certain .NET COM server DLLs outside the referring executables locations?

Update:

Researching further today, I realized that (despite the very similar terminology), resolving the ActivationContext with a private SxS and resolving the location of .NET DLLs which serve for a COM callable wrapper instantiation are two completely distinct and separated mechanisms. I mostly got that from these 2 and some more of Jufeng Zhang's brilliant and in depth explaining blog articles:

The problem with the locating of the unregistered .NET assemblies (managed COM server DLLs) is, that this will only happen inside the applications deployment directory and below.

Using any method like specifying a <codebase> or <probing> element inside the configuration <runtime> section pointing outside the the directory where the .config file is deployed, simply doesn't work.

I verified that using the Sysinternals Process Monitor and the Fusion log viewer tool2.

I'm not posting that as a final answer, because I'll try next somehow to trick that .NET mechanism to locate the managed COM server DLLs, using an assembly manifest, or native DLL specifying the dependencies and <probing> / <codebase> element to redirect the locating mechanism.

As a last resort (sic!) it seems to be even possible to provide your own customized appDomainManagerAssembly and appDomainManagerType in the application configuration under the <runtime> element.


Update II:

I'm afraid we have to go for managing the AppDomain ourselves using the CLR API from a native CLR Host.

Needs further investigation. One promising resource how to do that I found here:

"Customizing the Microsoft .NET Framework Common Language Runtime"


1) Excuse the German error messages please. I don't have an English version compiler at hand. But the translation given at google should work well.

2) So the question about better tools for diagnosing the problems, can be considered as solved.

c#
.net
delphi
com
winsxs
asked on Stack Overflow Jan 16, 2018 by (unknown user) • edited Aug 4, 2020 by Jason Aller

1 Answer

5
  • Is it even possible to provide a deployment structure like mentioned above, and maintain certain .NET COM server DLLs outside the referring executables locations?

It's definitely not possible(!) to resolve any assemblies provided for the intrinsic CLR hosting mechanism outside the AppDomain's executable directory.

You can use the

<probing privatePath="<some directory below your executable's location>" />`

But the <probing> tag works differently for SxS resolving (appearing under the manifest <windows> tag), and the CLR's mechanism to instantiate COM Callable Wrappers appearing under the <runtime> tag.


It's even undocumented, but specifying

<windows>
    <probing privatePath="../<xxx>" />
</windows>

for resolving the SxS dependencies supports relative paths for <xxx> up to 3 ../ parent directory levels from your executable's location works for any native COM server, while

<runtime>
    <probing privatePath="../<xxx>" />
    <!--                  ^^^ -->
</runtime>

or

<runtime>
    <codebase href="../<xxx>/xyz.dll" version="1.0.0.0"/>
    <!--            ^^^ -->
</runtime>

won't allow you to specify assembly locations pointing to locations upwards outside your AppDomain's hosting directory using the standard windows .NET mechanisms to resolve candidates to be instantiated as COM Callable Wrappers (hosted by the mscoreee.dll).
Descending deeper from your executable's deployment directory works well and as intended.


One way (probably the easiest) to intercept the CLR probing mechanism, is to provide a custom AppDomainManager implementation and specify it in the <appDomainManagerAssembly> and <appDomainManagerType> elements of the application configuration file:

 <configuration>
     <runtime>
          <appDomainManagerAssembly value="MyAppDomainMgr" />
          <appDomainManagerType value="MyAppDomainMgr.MyCustomAppDomainMgr, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
     </runtime>
 <configuration>

The implementation of the MyAppDomainMgr.MyCustomAppDomainMgr class should be in a .NET assembly, e.g. written in C#:

namespace MyAppDomainMgr 
{
    [ComVisible(true)]
    public class MyCustomAppDomainMgr : AppDomainManager
    {
        public MyCustomAppDomainMgr()
        {
        }

        public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
        {
            Console.Write("Initialize new domain called:  ");
            Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
            InitializationFlags = 
                AppDomainManagerInitializationOptions.RegisterWithHost;

            // Several ways to control settings of the AppDomainSetup class,
            // or add a delegate for the AppDomain.CurrentDomain.AssemblyResolve 
            // event.
         }
     }
 }

As soon your unmanaged application tries to access some COM interface (COM Callable Wrapper) through the CLR (i.e. a call to CoCreateInstance()), the MyCustomAppDomainMgr class will be instantiated and the InitializeNewDomain() function is called first.

The least intrusive way seems to be to add that delegate function:

public override void InitializeNewDomain(AppDomainSetup appDomainInfo)
{
    // ...
    AppDomain.CurrentDomain.AssemblyResolve += 
        new ResolveEventHandler(MyCustomAssemblyResolver);
}

static Assembly MyCustomAssemblyResolver(object sender, ResolveEventArgs args) 
{
    // Resolve how to find the requested Assembly using args.Name
    // Assembly.LoadFrom() would be a good way, as soon you found 
    // some matching Assembly manifest or DLL whereever you like to look up for it
}

The resulting assembly (MyAppDomainMgr.dll), must be placed beneath the unmanaged executable application.


answered on Stack Overflow Jan 18, 2018 by (unknown user) • edited Jan 18, 2018 by (unknown user)

User contributions licensed under CC BY-SA 3.0