Обратный вызов из.Net COM dll клиенту Delphi в COM без регистрации (параллельный)
TLDR: я пытаюсь вызвать асинхронные обратные вызовы из.Net COM dll для Delphi клиента.exe, но они не работают должным образом в COM без регистрации, хотя синхронные обратные вызовы работают, а также асинхронные обратные вызовы работают, когда это не рег-свободный COM.
Мой глобальный случай заключается в том, что у меня есть иностранный dll.Net с закрытым исходным кодом, который выставляет некоторые публичные события. Мне нужно передать эти события в приложение Delphi. Поэтому я решил создать промежуточный.dll, который будет работать в качестве COM-моста между моим приложением и другой DLL. Это работало очень хорошо, когда моя dll зарегистрирована через regasm, но все ухудшается, когда я переключаюсь на reg-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, используя Component > Import Component > Import a Type Library
который дал мне
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.
Итак, я запустил его, нажал кнопки "Синхронизация" и "Асинхронизация", и все заработало как положено. Обратите внимание, как идентификаторы потока задачи появляются после строки 'End async call' (также с некоторой задержкой из-за Thread.Sleep
):
Конец первой части. Теперь я перешел на использование 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. Но я не уверен, что и как. Любая помощь будет оценена!
2 ответа
Вы должны зарегистрировать 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- ХХХХ-ХХХХХХХХХХХХ}). Этот 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=""/>
Это слишком долго, чтобы быть комментарием, поэтому публиковать его как ответ.
Указатель на интерфейсы COM никогда не должен быть доступен из другой квартиры COM без надлежащего маршалинга. В этом случае, this.handler
(скорее всего) COM-объект STA, созданный в потоке STA Delphi. Затем он напрямую вызывается из потока потока пула.NET MTA внутри Task.Run
без какого-либо COM-маршалинга. Это нарушение жестких правил COM, описанных здесь. ИНФОРМАЦИЯ: Описания и работа моделей потоков OLE.
То же самое относится и к управляемому прокси-серверу RCW, оборачивающему интерфейс COM на стороне.NET. RCW просто перенаправит вызов метода из управляемого в неуправляемый код, но ничего не сделает с маршалингом COM.
Это может привести к неприятным сюрпризам, особенно если ОП получает доступ к пользовательскому интерфейсу приложения 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 должны быть зарегистрированы или предоставлены через параллельный манифест, как объясняет ответ Пауло Мадейры.
В качестве альтернативы, обычай dispinterface
можно использовать (в этом случае все вызовы будут проходить через IDispatch
с OLE Automation marshaler) или любым другим стандартным интерфейсом COM, известным стандартному COM-маршалеру. Я часто использую IOleCommandTarget
для простых обратных вызовов ничего не нужно регистрировать.