繁体   English   中英

从.Net COM dll回拨到Delphi客户端,无需注册(并排)COM

[英]Callback from .Net COM dll to Delphi client in registration-free (side-by-side) COM

TLDR :我正在尝试从.Net COM dll调用异步回调到Delphi客户端.exe,但这些似乎在免注册COM中无法正常工作,而同步回调确实有效,并且异步回调正在工作时不是一个免注册的COM。


我的全球案例是,我有一个外国的闭源.Net dll暴露了一些公共事件。 我需要将这些事件传递给Delphi应用程序。 所以我决定制作一个中间的.dll,它可以作为我的app和另一个dll之间的COM桥。 当我的dll通过regasm注册时它工作得很好,但是当我切换到免注册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客户端。 我使用Component > Import Component > Import a Type Library来创建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;

并创建了一个处理程序和一些带有两个按钮和备忘录的Form来测试事物:

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如何在“结束异步调用”行之后出现(由于Thread.Sleep也有一些延迟):

所有工作都通过注册-COM

第一部分结束。 现在我切换到使用Rregistration-free(并排)COM。 根据这个答案,我将dependentAssembly部分添加到我的Delphi app清单中:

<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并运行应用程序。 我发现只有回调的同步部分正在工作:

在此输入图像描述

编辑: 请注意,您必须使用/tlb选项取消注册,否则它将继续在本地计算机上工作,就好像dll仍然已注册( 请参阅参考资料)。

我已经厌倦了很多事情,我不知道下一步该做什么。 我盯着怀疑初始方法根本不起作用,我需要在Delphi应用程序端实现一些线程。 但我不确定是什么以及如何。 任何帮助,将不胜感激!

您必须注册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非常适合你的账单,你可以把它设置为post build事件。

最后,您可以保留单独的类型库文件,也可以将其嵌入到程序集中。 我建议你把它分开,如果你的组装很大,那就更好了。

无论哪种方式,您都需要将其放入清单中。 这是使用单独的.TLB文件的示例:

    <file name="ComDllNet.tlb">
        <typelib tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                 version="1.0"
                 helpdir="."
                 flags=""/>
    </file>

如果嵌入了类型库,请将以下内容添加为<file name="ComDLLNet.dll"/>元素的<file name="ComDLLNet.dll"/>

        <typelib tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                 version="1.0"
                 helpdir="."
                 flags=""/>

这太长了,无法发表评论,因此将其作为答案发布。

如果没有正确的编组,就不应该从不同的COM公寓访问指向COM接口的指针 在这种情况下, this.handler (很可能)是在Delphi的STA线程上创建的STA COM对象。 然后它直接从Task.Run的.NET MTA池线程线程Task.Run ,没有任何类型的COM封送。 这违反了COM硬规则,这里概述了INFO:OLE线程模型的描述和工作原理

关于在.NET端包装COM接口的托管RCW代理也是如此。 RCW将只调度从托管代码到非托管代码的方法调用,但它不会对COM编组执行任何操作。

这可能导致各种令人讨厌的惊讶,特别是如果OP访问处理程序内部的Delphi应用程序的handler.Callback

现在, handler对象可能会聚合Free Threaded Marshaler(这将有自己的规则可以遵循 ,我怀疑是OP代码的情况)。 就这样,指向handler对象的指针确实将由FTM解组为同一指针 但是,从另一个线程调用该对象的服务器代码(即, Task.Run(() => { ... this.handler.Callback(GetThreadInfo() ...})不应该假定COM对象是空闲的 -螺纹,它仍然应该做正确的编组。如果幸运的话,直接指针将在解组时返回。

有很多方法可以进行编组:

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

当然,要使上述编组方法正常工作,正确的COM代理/存根类应该通过并排清单进行注册或配置,正如Paulo Madeira的答案所解释的那样。

或者,可以使用自定义dispinterface (在这种情况下,所有调用都将通过带有OLE自动化封送程序的IDispatch )或标准COM封送程序已知的任何其他标准COM接口。 我经常使用IOleCommandTarget进行简单的回调,它不需要注册任何东西。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM