Обратный вызов из.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

Конец первой части. Теперь я перешел на использование 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-объект является свободным потоком, и он все равно должен выполнять правильный маршалинг. Если повезет, прямой указатель будет возвращен при демаршалинге.

Есть несколько способов сделать маршалинг:

Конечно, чтобы вышеприведенные методы маршалинга работали, правильные классы прокси / заглушки COM должны быть зарегистрированы или предоставлены через параллельный манифест, как объясняет ответ Пауло Мадейры.

В качестве альтернативы, обычай dispinterface можно использовать (в этом случае все вызовы будут проходить через IDispatch с OLE Automation marshaler) или любым другим стандартным интерфейсом COM, известным стандартному COM-маршалеру. Я часто использую IOleCommandTarget для простых обратных вызовов ничего не нужно регистрировать.

Другие вопросы по тегам