如何提供将 .NET Dll 正确定位为 COM 提供程序的私有并排清单?

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

我正在研究使用简单的程序集清单文件提供的私人注册免费 WinSxS 的配置,以将 Delphi 可执行文件(COM 客户端)和 .NET (C#) COM 可见 DLL 拼接在一起部署和运行时。

我已经特别研究了 MSDN "Interoperating with Unmanaged Code", the sections about "COM Callable Wrapper" and "How to: Configure .NET Framework-Based COM Components for Registration-Free Activation" 上提供的文档。

经过一个多星期的研究,并被(重新)引导到文档不足的循环中,我决定把我的第一个问题放在这里。

计划的部署结构如下所示:

./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

这是关于 Delphi 本机可执行文件和 C# .NET COM 服务器 DLL 的实现的简短概述(我省略了本机 COM 服务器的示例,因为这些东西已经运行良好毫无疑问)。
我主要按照 "Registration-Free Activation of COM Components: A Walkthrough" 提供的内容进行操作。主要区别是我使用 Delphi 而不是 C、C++ 或旧的 VB 作为 native 客户端。

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

(嵌入资源 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

(部署在与可执行文件相同的文件位置)

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

CSharpCOMDll.dll

(将部署在SharedLibs\MyCompany.Libs.Set1目录)

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

(嵌入到资源 ID 为 2 的 DLL 中)

<?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>

最后是从 TestDllConsoleApp.manifest dependency 条目解析的程序集清单:

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>

看来我已经完成了一半,但仍然无法诊断出实际问题。

现在有两种失败变体(请注意,在可执行文件旁边部署托管 COM 服务器 DLL 而不是引用已解析的清单目录就可以正常工作,并且预期):

  1. 我完全删除了全局清单中的proxyStubClsid32属性:

    • 启动可执行文件以异常结束
      EOleSysError: Error in dll, clsid = {4CD39F25-0EB9-4CD0-9B4C-6F5DB5C14805}

    • 调试异常导致 HRESULT

         Error in the DLL (Exception from HRESULT: 0x800401F9 (CO_E_ERRORINDLL))
      
  2. 我在全局清单中提供了一个proxyStubClsid32属性:

    • 我不确定该属性实际需要哪个 GUID。
      正如文档中提到的那样,它自然似乎是 comClass 元素 clsid 属性中提到的相应“co class ID”(CLSID)。
    • 我或者尝试从那里生成的 ,pas 文件提供 LIBID GUID。

    这两种变体都给我留下了一个非常无用的错误,可以使用 sxstrace 工具追踪1:

     ...
     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.
    

    请注意,没有像

    这样简洁的error/info消息
      ... cannot resolve assembly XY ...
    

    激活上下文生成搞砸之前。有很多参考资料表明了这种特殊的错误情况。
    此外,普遍提到缺少 Visual C++ 可再发行框架 在这里也无济于事。我是从 Delphi 打来的,那是不一样的。

  3. 另一次尝试显式引用 CSharpCOMDll.dll(可执行清单中的另一个依赖项),并将其放入 SharedLibs 成功创建 激活上下文,但失败并出现与之前略有不同的异常

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

这里有没有人知道如何直接做我想做的事情,或者可以做些什么(除了 sxstrace)来更深入地诊断问题。

我几乎可以肯定,一定可以提供这样的 部署


TL;DR;


更新:

今天进一步研究,我意识到(尽管术语非常相似),用私有 SxS 解析 ActivationContext 并解析为 COM 服务的 .NET DLL 的位置可调用包装器实例化是两个完全不同且独立的机制。我主要是从这 2 篇和更多 Jufeng Zhang's 精彩而深入的解释博客文章中得到的:

未注册的 .NET 程序集(托管 COM 服务器 DLL)的定位问题在于,这只会发生在应用程序部署目录内及以下。

使用任何方法,例如在配置 <runtime> 部分中指定 <codebase><probing> 元素,指向部署 .config 文件的目录之外,根本不会没用。

我验证了使用 Sysinternals Process Monitor 和 Fusion 日志查看器工具2

我不会将其作为最终答案发布,因为接下来我将尝试以某种方式欺骗 .NET 机制来定位托管 COM 服务器 DLL,使用程序集清单或指定依赖项的本机 DLL 和 <probing> / <codebase> 元素重定向定位机制。

作为最后的手段(原文如此!),似乎甚至可以在 <runtime> 元素下的应用程序配置中提供您自己的自定义 appDomainManagerAssemblyappDomainManagerType


更新二:

恐怕我们必须使用来自本机 CLR 主机的 CLR API 自己管理 AppDomain

需要进一步调查。我在这里找到了一个很有前途的资源:

"Customizing the Microsoft .NET Framework Common Language Runtime"


1) 请原谅德语错误消息。我手边没有英文版的编译器。但是 google 给出的翻译应该可以正常工作。

2) 所以关于更好的诊断问题的工具的问题,可以认为已经解决了。

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

绝对不可能 (!) 解析为 AppDomain 的可执行目录之外的内部 CLR 托管机制提供的任何程序集。

您可以使用

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

但是 <probing> 标签对于 SxS 解析(出现在清单 <windows> 标签下)和 CLR 实例化 COM Callable Wrappers 的机制不同出现在 <runtime> 标签下。


它甚至没有记录,但指定

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

为了解决 SxS 依赖关系,支持 <xxx> 的相对路径,最多 3 个 ../ 父目录级别,可执行文件的位置适用于任何 本地 COM 服务器,而

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

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

不允许您使用标准 windows .NET 机制来指定指向 AppDomain 托管目录之外的位置的程序集位置,以将候选人解析为实例化为 COM Callable Wrappers(由 mscoreee.dll 托管)。
从您的可执行文件的部署目录向下深入,效果很好,符合预期。


拦截 CLR 探测机制的一种方法(可能是最简单的方法)是提供自定义 AppDomainManager 实现并在应用程序配置文件的 <appDomainManagerAssembly> and <appDomainManagerType> 元素中指定它:

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

MyAppDomainMgr.MyCustomAppDomainMgr class 的实现应该在 .NET 程序集中,例如用 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.
         }
     }
 }

一旦您的非托管应用程序尝试通过 CLR 访问某些 COM 接口(COM 可调用包装器)(即调用 CoCreateInstance()),MyCustomAppDomainMgr class 将被实例化InitializeNewDomain() 函数首先被调用。

侵入性最小的方法似乎是添加委托函数:

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
}

生成的程序集 (MyAppDomainMgr.dll) 必须放置在非托管可执行应用程序之下。