Search code examples
c#.netdelphicomregistration-free-com

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


TLDR: I'm trying to call async callbacks from a .Net COM dll to Delphi client .exe, but these does not seem to work properly in registration-free COM, while synchronous callbacks do work, and also async callbacks are working when it's not a reg-free COM.


My global case is that I'm having a foreign closed-source .Net dll that exposes some public events. I need to pass these events to Delphi app. So I decided to make an intermediate .dll that would work as a COM bridge between my app and that another dll. It worked just fine when my dll is registered via regasm, but things are getting worse when I switch to reg-free COM. I shortened my case to small reproducible example which does not depend on the other dll, so I'll be posting it below.

Based on this answer I made a public interface ICallbackHandler which I expect to get from Delphi client app:

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());
                }
            });
        }
    }
}

Then, I gave a strong name to dll, and registered it via Regasm.exe.

Now I turned to Delphi client. I create the tlb wrapper code using Component > Import Component > Import a Type Library which gave me

  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;

And created a handler and some Form with two buttons and memo to test things:

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.

So, I run it, pressed 'sync' and 'async' buttons and everything worked as expected. Note how the thread ids of a Task comes after 'End async call' line (also with some delay because of Thread.Sleep):

all works via registration-COM

End of part one. Now I switched to using Rregistration-free (side-by-side) COM. Based on this answer I added dependentAssembly part to my Delphi app manifest:

<dependency>
    <dependentAssembly>
        <assemblyIdentity name="ComDllNet" version="1.0.0.0" publicKeyToken="f31be709fd58b5ba" processorArchitecture="x86"/>
    </dependentAssembly>
</dependency>

Using the mt.exe tool I generated a manifest for my 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>

Then I unregistered the dll and run the app. And I found that only synchronous parts of the callbacks are working:

enter image description here

Edit: Note that you have to unregister with /tlb option, otherwise it will continue working on local machine, as if dll was still registered (see).

I tired a number of things already, and I'm not sure what to do next. I'm staring to suspect that the initial approach should not work at all and I need to implement some threading on the Delphi app side. But I'm not sure what and how. Any help would be appreciated!


Solution

  • You have to register the ICallbackHandler interface. So, in the same file where you have the clrClass element, but as a sibling of the file elements, add:

        <comInterfaceExternalProxyStub iid="{B6597243-2CC4-475B-BF78-427BEFE77346}"
                                       name="ICallbackHandler"
                                       tlbid="{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}"
                                       proxyStubClsid32="{00020424-0000-0000-C000-000000000046}"/>
    

    This tells COM to use an external proxy/stub, the type library marshaler ({00020424-0000-0000-C000-000000000046}), and it tells the type library marshaler to look for your type library ({XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}). This GUID is your assembly's GUID, found in your project's properties (check AssemblyInfo.cs).

    You need to generate this type library. Since you want registration-free COM, I think TLBEXP.EXE fits the bill perfectly, you can set it up as a post build event.

    Finally, you can keep a separate type library file or you can embed it in your assembly. I advise you keep it separate, even more so if your assembly is big.

    Either way, you need to put this into the manifest. Here's an example using a separate .TLB file:

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

    If you embed the type library, add the following as a child of the <file name="ComDLLNet.dll"/> element:

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