在无需注册的(并存)COM中,从.Net COM dll到Delphi客户端的回调

10

TLDR: 我正试图从一个 .Net COM dll 到 Delphi client .exe 调用异步回调函数,但在注册-free COM 中似乎无法正常工作,而同步回调可以正常工作,当不是 reg-free COM 时也可以正常工作。


我的情况是有一个外部闭源的 .Net dll,它公开了一些公共事件。我需要将这些事件传递给 Delphi 应用程序。因此,我决定创建一个中间 .dll,它将作为我的应用程序和另一个 dll 之间的 COM 桥梁。当我的 dll 通过 regasm 注册时,它能够正常工作,但是当我切换到注册-free COM 时,情况就变得更糟了。我缩小了问题范围,以便在下面发布一个与其他 dll 无关的简单可重现示例。

根据这个答案,我制作了一个公共接口ICallbackHandler,我希望从 Delphi 客户端应用程序中获得:

namespace ComDllNet
{
    [ComVisible(true)]
    [Guid("B6597243-2CC4-475B-BF78-427BEFE77346")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface ICallbackHandler
    {
        void Callback(int value);
    }

    [ComVisible(true)]
    [Guid("E218BA19-C11A-4303-9788-5A124EAAB750")]
    public interface IComServer
    {
        void SetHandler(ICallbackHandler handler);
        void SyncCall();
        void AsyncCall();
    }

    [ComVisible(true)]
    [Guid("F25C66E7-E9EF-4214-90A6-3653304606D2")]
    [ClassInterface(ClassInterfaceType.None)]
    public sealed class ComServer : IComServer
    {
        private ICallbackHandler handler;
        public void SetHandler(ICallbackHandler handler) { this.handler = handler; }

        private int GetThreadInfo()
        {
            return Thread.CurrentThread.ManagedThreadId;
        }

        public void SyncCall()
        {
            this.handler.Callback(GetThreadInfo());
        }

        public void AsyncCall()
        {
            this.handler.Callback(GetThreadInfo());
            Task.Run(() => {
                for (int i = 0; i < 5; ++i)
                {
                    Thread.Sleep(500);
                    this.handler.Callback(GetThreadInfo());
                }
            });
        }
    }
}

然后,我给dll赋予了强名称,并通过Regasm.exe进行了注册。

现在我转到Delphi客户端。我使用组件 > 导入组件 > 导入类型库创建了tlb包装代码,它给了我:


  ICallbackHandler = interface(IUnknown)
    ['{B6597243-2CC4-475B-BF78-427BEFE77346}']
    function Callback(value: Integer): HResult; stdcall;
  end;
  IComServer = interface(IDispatch)
    ['{E218BA19-C11A-4303-9788-5A124EAAB750}']
    procedure SetHandler(const handler: ICallbackHandler); safecall;
    procedure SyncCall; safecall;
    procedure AsyncCall; safecall;
  end;
  IComServerDisp = dispinterface
    ['{E218BA19-C11A-4303-9788-5A124EAAB750}']
    procedure SetHandler(const handler: ICallbackHandler); dispid 1610743808;
    procedure SyncCall; dispid 1610743809;
    procedure AsyncCall; dispid 1610743810;
  end;

创建了一个处理程序,以及一些带有两个按钮和备忘录的表单,用于测试:

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComDllNet_TLB, StdCtrls;

type
  THandler = class(TObject, IUnknown, ICallbackHandler)
  private
    FRefCount: Integer;
  protected
   function Callback(value: Integer): HResult; stdcall;

   function QueryInterface(const IID: TGUID; out Obj): HRESULT; stdcall;
   function _AddRef: Integer; stdcall;
   function _Release: Integer; stdcall;
  public
    property RefCount: Integer read FRefCount;
  end;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    syncButton: TButton;
    asyncButton: TButton;
    procedure FormCreate(Sender: TObject);
    procedure syncButtonClick(Sender: TObject);
    procedure asyncButtonClick(Sender: TObject);
  private
    { Private declarations }
    handler : THandler;
    server : IComServer;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

function THandler._AddRef: Integer;
begin
  Inc(FRefCount);
  Result := FRefCount;
end;

function THandler._Release: Integer;
begin
  Dec(FRefCount);
  if FRefCount = 0 then
  begin
    Destroy;
    Result := 0;
    Exit;
  end;
  Result := FRefCount;
end;

function THandler.QueryInterface(const IID: TGUID; out Obj): HRESULT;
const
  E_NOINTERFACE = HRESULT($80004002);
begin
  if GetInterface(IID, Obj) then
    Result := 0
  else
    Result := E_NOINTERFACE;
end;

function THandler.Callback(value: Integer): HRESULT;
 begin
  Form1.Memo1.Lines.Add(IntToStr(value));
  Result := 0;
 end;

procedure TForm1.FormCreate(Sender: TObject);
 begin
  handler := THandler.Create();
  server := CoComServer.Create();
  server.SetHandler(handler);
 end;

procedure TForm1.syncButtonClick(Sender: TObject);
 begin
  Form1.Memo1.Lines.Add('Begin sync call');
  server.SyncCall();
  Form1.Memo1.Lines.Add('End sync call');
 end;

procedure TForm1.asyncButtonClick(Sender: TObject);
 begin
  Form1.Memo1.Lines.Add('Begin async call');
  server.AsyncCall();
  Form1.Memo1.Lines.Add('End async call');
 end;

end.

所以,我运行它,按下“同步”和“异步”按钮,一切都按预期工作。请注意,任务的线程ID出现在“End async call”行之后(还有一些延迟,因为使用了Thread.Sleep):

all works via registration-COM

第一部分结束。现在我转而使用Registration-free(并行)COM。根据这个答案,我在我的Delphi应用程序清单中添加了dependentAssembly部分:

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

我使用mt.exe工具为我的dll生成了一个清单:

<?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="f31be709fd58b5ba" processorArchitecture="x86"/>
    <clrClass clsid="{F25C66E7-E9EF-4214-90A6-3653304606D2}" progid="ComDllNet.ComServer" threadingModel="Both" name="ComDllNet.ComServer" runtimeVersion="v4.0.30319"/>
    <file name="ComDllNet.dll" hashalg="SHA1"/>
</assembly>

然后我注销了dll并运行应用程序。我发现只有回调函数中同步的部分起作用了:

enter image description here

编辑:请注意,您必须使用/tlb选项注销,否则它将继续在本地计算机上工作,就像dll仍然注册一样(请参见)。

我已经尝试了很多方法,但不确定接下来该怎么做。我开始怀疑最初的方法根本不会起作用,需要在Delphi应用程序端实现一些线程控制。但我不确定该如何操作。如果有任何帮助,将不胜感激!

2个回答

6

您需要注册 ICallbackHandler 接口。因此,在具有 clrClass 元素的同一文件中,但作为file元素的兄弟元素,请添加:

    <comInterfaceExternalProxyStub iid="{B6597243-2CC4-475B-BF78-427BEFE77346}"
                                   name="ICallbackHandler"
                                   tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                                   proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"/>

这段话告诉COM使用外部代理/存根,类型库编组程序({00020424-0000-0000-C000-000000000046}),并告诉类型库编组程序查找您的类型库({XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX})。这个GUID是您程序集的GUID,在项目属性中找到(检查AssemblyInfo.cs)。
您需要生成此类型库。由于您想要无需注册的COM,我认为TLBEXP.EXE非常适合这个任务,您可以将其设置为后置构建事件。
最后,您可以保留单独的类型库文件,也可以将其嵌入到程序集中。我建议您保留它作为单独的文件,特别是如果您的程序集很大。
无论哪种方式,您都需要将其放入清单中。以下是一个使用单独的.TLB文件的示例:
    <file name="ComDllNet.tlb">
        <typelib tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                 version="1.0"
                 helpdir="."
                 flags=""/>
    </file>

如果你要嵌入类型库,需要将以下内容添加为<file name="ComDLLNet.dll"/>元素的子级:
        <typelib tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                 version="1.0"
                 helpdir="."
                 flags=""/>

1
注册代理是正确的做法,但@Mikhail也不应该直接从池线程调用this.handler.Callback(GetThreadInfo()),而没有进行COM封送。除非他的Delphi端期望在随机线程上接收回调。 - noseratio - open to work
@Noseratio:看起来确实是这种情况。你能指导我正确的封送实现吗? - Mikhail
@Mikhail,我认为最简单的两种方法是CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream(示例在这里)和全局接口表(GIT,示例在这里)。 - noseratio - open to work
@Mikhail,是的,请将此信息添加到引用您的DLL的清单中。如果您将清单嵌入DLL中,则它将在DLL的清单中。 - acelent
1
保罗,来自那个链接:“如果在一个应用程序域或公寓中创建了一个RCW,然后将引用传递到另一个应用程序域或公寓,则将使用对第一个对象的代理。”也就是说,RCW不执行任何COM封送。这正是我想让@Mikhail知道的问题。我的评论太长了,作为 答案 发布它。 - noseratio - open to work
显示剩余4条评论

2

这段评论太长了,所以我将其作为答案发布。

指向COM接口的指针不应在未经适当封送的情况下从不同的COM单元中访问。在这种情况下,this.handler(很可能)是在Delphi的STA线程上创建的STA COM对象。然后,它直接从.NET MTA池线程内部的Task.Run调用,没有任何COM封送。这违反了COM的硬规则,在此处概述:OLE线程模型的说明和工作原理

对于在.NET端包装COM接口的托管RCW代理也是如此。RCW只会将方法调用从托管代码封送到非托管代码,但它不会对COM封送做任何处理。

这可能导致各种令人不快的惊喜,特别是如果OP在handler.Callback中访问Delphi应用程序的UI。

现在,handler对象可能聚合自由线程封送程序(FTM)(这将有其自己的遵循的规则,但我怀疑这在OP代码中并不是这种情况)。即使如此,调用来自另一个线程的对象(即Task.Run(() => { ... this.handler.Callback(GetThreadInfo() ...}))仍不应假定COM对象是自由线程的,并且仍应进行正确的封送。如果幸运,解封时将返回直接指针。

有许多方法可以进行封送:

  • CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream
  • CoMarshalInterface/CoUnmarshalInterface
  • 全局接口表(GIT)
  • CreateObjrefMoniker/BindMoniker
  • 等等。

当然,为了使上述封送方法起作用,应注册或通过并行清单提供正确的COM代理/存根类,正如Paulo Madeira的答案所解释的那样。

另外,可以使用自定义的dispinterface(在这种情况下,所有调用都通过带有OLE Automation marshaler的IDispatch进行),或者任何标准的COM接口(已知标准COM marshaler)。我经常使用IOleCommandTarget进行简单的回调,它不需要注册任何内容。


你是否尝试过在一个公寓中获取RCW(实际对象或代理/存根),并在另一个公寓中使用它?我从未担心过这个问题,而且这篇博客文章非常明确地说明了RCW所做的后台工作量。 - acelent
@PauloMadeira,我已经做了很多次。在不同公寓间调用相同RCW上的方法就像在不同公寓间调用基础原始COM接口指针上的方法一样好。也就是说,在这方面,RCW是透明的,其余真的取决于特定的COM对象。如果您确信该对象以自由线程实现(我确定“handler”不属于此类),则可能可以正常工作。 - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接