[英]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
也有一些延迟):
第一部分结束。 现在我切换到使用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
。 CreateObjrefMoniker
/ BindMoniker
。 当然,要使上述编组方法正常工作,正确的COM代理/存根类应该通过并排清单进行注册或配置,正如Paulo Madeira的答案所解释的那样。
或者,可以使用自定义dispinterface
(在这种情况下,所有调用都将通过带有OLE自动化封送程序的IDispatch
)或标准COM封送程序已知的任何其他标准COM接口。 我经常使用IOleCommandTarget
进行简单的回调,它不需要注册任何东西。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.