10

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


我的全球案例是我有一个公开一些公共事件的外国闭源 .Net dll。我需要将这些事件传递给 Delphi 应用程序。所以我决定制作一个中间 .dll,作为我的应用程序和另一个 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 注册它。

现在我转向德尔福客户端。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;

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

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.

所以,我运行它,按下“同步”和“异步”按钮,一切都按预期工作。请注意 Task 的线程 ID 是如何出现在“结束异步调用”行之后的(也有一些延迟,因为Thread.Sleep):

所有作品通过注册-COM

第一部分结束。现在我切换到使用无 Rregistration(并排)COM。基于这个答案dependentAssembly,我在我的 Delphi 应用程序清单中添加了一部分:

<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 应用程序端实现一些线程。但我不确定是什么以及如何。任何帮助,将不胜感激!

4

2 回答 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=""/>
于 2015-02-26T11:32:04.080 回答
2

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

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

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

这可能会导致各种令人讨厌的意外,尤其是当 OP 访问 Delphi 应用程序的 UI 内部时handler.Callback

现在,该handler对象可能聚合了 Free Threaded Marshaler(这将有自己的规则要遵循,我怀疑 OP 代码的情况是否如此)。顺其自然,指向对象的指针确实会被 FTMhandler解组为同一个指针。但是,从另一个线程调用对象的服务器代码(即,Task.Run(() => { ... this.handler.Callback(GetThreadInfo() ...})永远不应该假设 COM 对象是自由线程的,它仍然应该进行正确的编组。如果幸运的话,直接指针将在解组时返回。

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

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

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

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

于 2015-02-26T23:55:40.620 回答