Mar 21, 2015

Some COM for you - Chapter 1

In which Piglet Meets a Heffalump Delphi meets .Net


"I saw one once," said Piglet. "At least, I think I did," he said. "Only perhaps it wasn't."
A. A. Milne


If you ever programmed for Windows, you should have heard weird stories of evil COM that lurks there in shadows waiting to drag you into the abyss of marshalling and IUnknowns...

As for me, I remembered Joel saying that when Microsoft first presented COM, they were proud to say that "it requires at least 20 000 lines of code just to get started". (Strangely, I can't find that line on his site anymore, can somebody help me with it?) So I never had any will to learn COM through (which may be not the best part of me), but anyway.

When I found that I need to use some COM libraries in a desktop app I decided to study C# because .Net has been said to have native COM support and I should not have to worry about the details. (And this was for the better, actually! I was prejudiced against C# knowing that Gosling blamed it for having unsafe code, but it turned out that C# is actually a better Java, and I like it!).

So when I recently found that I have to make a C# library to work with Delphi app and the only way to do it is COM, I started preparing for worst. Below is a story of how to make this work, which I'd like to keep in case I'll need to do something like that again.

The problem just reminded me about this promising energy source



So here are some details of the task. I have a closed-source C# dll, which besides everything else, makes some async callbacks to hosting code. The task is to call dll methods from Delphi 2010 host program and to receive callbacks from dll in Delphi code.

The task would have been much simpler if it was not a managed dll. Delphi is pretty much OK working with native .dlls, and I was doing it few times back in my university days. But this time, the dll is managed code. So it seems that COM is the only way through. (Actually, there's some other way to call managed code from unmanaged, but it looks so weird, that I don't like to touch it at all. And it does not solve the problem of making callbacks.)

To not bother you with details of the library that I was to interact with, here's the code of a test ComDllNet.dll I created while looking for a way through the troubles I'll describe below:

namespace ComDllNet
{
    public interface ICallbackHandler
    {
        void Callback(int value);
    }

    public interface IComServer
    {
        void SetHandler(ICallbackHandler handler);
        void SyncCall();
        void AsyncCall();
    }

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

Now the idea is pretty much simple. We have a class (ComServer) that we want to expose via COM, and we want it to make callbacks via ICallbackHandler interface both in synchronous (ComServer.SyncCall()) and asynchronous (ComServer.AsyncCall()) ways with some pay-load (). At first I was trying to use delegates to just pass the reference to a callback function, but for some reason it did not work from Delphi side. Anyway, the solution is based on this SO answer.

Now, the first thing to do it to specify GUIDs for the assembly, interfaces and classes, and also to specify some other attributes:

    //don't expose everything to COM, only expose what you manually specify
    [assembly: ComVisible(false)]
    
    // The following GUID is for the ID of the typelib if this project is exposed to COM
    [assembly: Guid("XXXXXXXX-XXXX-XXXX-XXXX-000000000001")]

    [ComVisible(true)]
    [Guid("XXXXXXXX-XXXX-XXXX-XXXX-000000000002")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    public interface ICallbackHandler
    {
        void Callback(int value);
    }

    [ComVisible(true)]
    [Guid("XXXXXXXX-XXXX-XXXX-XXXX-000000000003")]
    public interface IComServer
    {
        void SetHandler(ICallbackHandler handler);
        void SyncCall();
        void AsyncCall();
    }

    [ComVisible(true)]
    [Guid("XXXXXXXXX-XXXX-XXXX-XXXX-000000000004")]
    [ClassInterface(ClassInterfaceType.None)]
    public sealed class ComServer : IComServer
    {
....

You'll get your own GUIDs, so I put Xes there but I keep the numbers in the end to keep track of what is what later.
Now what remains is to give your .dll a strong name. Which actually means just signing it using Properties->Signing page. Now, just build the .dll and first part is finished.


Next step is to register the .dll in GAC and COM registry. To do this, you can't use the RegSrv, as the .dll is not a native, but a managed one. We actually have to use RegAsm tool from .Net package:

C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe c:\Dev\Projects\ComTest\ComDllNet.dll /tlb: c:\Dev\Projects\ComTest\ComDllNet.tlb

You'll need administrative rights to run this properly.
Now the few not so funny things about this stuff. First of all, it looks like you'll have to register your .dll on every machine you want it to work (unless you use Registration-free COM, which is a reason I'm writing this post, and is explained in the next chapter). So you will probaly want to put the lines above into a .bat file at least. But you may notice that you don't have RegAsm in your PATH so you'll have to specify it's full path including the .Net platform version. Which is a bit weird to say least.

Next, you will have to pass an absolute path to your .dll to RegAsm, which is even more weird. Of cause, you may do some .bat magic to get it, but why should I bother in first place?

Anyway, the important part is the /tlb option that instructs RegAsm to create a COM type library file and register it in COM registry.

OK, now we should have the .dll registered both in GAC and COM. You may check GAC using gacutil from VS developer command prompt, and lookup COM with OLEView (runs from developer command prompt for me as well).

Next part is to attach to .dll from Delphi. To do this, you'll first need to create a TLB wrapper for the .dll. This is done via Component->Import Component menu. In the wizard you should choose Import a Type Library, even though your sense of reason tells you to choose Import .NET Assembly.(forget about reason anyway, you are doing COM now!). Next you'll have to locate your .dll in the list of all known COM assemblies. (If you don't find it, something failed on previous steps.) And finally choose where you want to get the wrapper code placed (Unit Dir Name). In the end you should get a ComDllNet_TLB.pas where interesting parts are like following:

const
  ComDllNetMajorVersion = 1;
  ComDllNetMinorVersion = 0;

  LIBID_ComDllNet: TGUID = '{XXXXXXXX-XXXX-XXXX-XXXX-000000000001}';
  IID_ICallbackHandler: TGUID = '{XXXXXXXX-XXXX-XXXX-XXXX-000000000002}';
  IID_IComServer: TGUID = '{XXXXXXXX-XXXX-XXXX-XXXX-000000000003}';
  CLASS_ComServer: TGUID = '{XXXXXXXXX-XXXX-XXXX-XXXX-000000000004}';
type
  ICallbackHandler = interface;
  IComServer = interface;
  IComServerDisp = dispinterface;
  ComServer = IComServer;
  ICallbackHandler = interface(IUnknown)
    ['{XXXXXXXX-XXXX-XXXX-XXXX-000000000002}']
    function Callback(value: Integer): HResult; stdcall;
  end;
  IComServer = interface(IDispatch)
    ['{XXXXXXXX-XXXX-XXXX-XXXX-000000000003}']
    procedure SetHandler(const handler: ICallbackHandler); safecall;
    procedure SyncCall; safecall;
    procedure AsyncCall; safecall;
  end;
....
  CoComServer = class
    class function Create: IComServer;
    class function CreateRemote(const MachineName: string): IComServer;
  end;

With this we should be able to make use of the .dll in a real Delphi app! First of all, let's create a class implementing ICallbackHandler. It's inherited from IUnknown, so we'll have to implement it as well:

...
interface

uses
  ...., ComDllNet_TLB;

type
  THandler = class(TObject, IUnknown, ICallbackHandler)
  private
    FRefCount: Integer;
  protected
   function Callback(value: Integer): HResult; stdcall;
   //IUnknown
   function QueryInterface(const IID: TGUID; out Obj): HRESULT; stdcall;
   function _AddRef: Integer; stdcall;
   function _Release: Integer; stdcall;
  public
    property RefCount: Integer read FRefCount;
  end;

implementation

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)); //the only actual code
  Result := 0;
 end;

And so we can make a test app with two buttons and a memo to check how things work:

....
type
  TForm1 = class(TForm)
    Memo1: TMemo;
    syncButton: TButton;
    asyncButton: TButton;
    procedure FormCreate(Sender: TObject);
    procedure syncButtonClick(Sender: TObject);
    procedure asyncButtonClick(Sender: TObject);
  private
    handler : THandler;
    server : IComServer;
  end;

var
  Form1: TForm1;

implementation

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;

So, everything is sent up and ready, so it's time to test! When I run the program and press buttons, I get the following results:

Things to note:
1. Synchronous callback is working and returning a thread ID.
2. Asynchronous callback is working, returning same thread ID as synchronous one in synchronous part, and after the calling code ends, it then prints ID of another thread with half-second delay!

The 2nd point is pretty much amazing, as we did not do anything about marshalling, or other inter-threading inter-op. Actually, it just should not work, but it does and it's what I wanted to achieve in the first place. So I decided to not dig any deeper for now.

The behaviour is exactly that I expected to achieve. And so let's end the prelude chapter here. Next one will be about the real thing - Registration-free COM.

No comments:

Post a Comment