2015-02-26 8 views
9

TLDR: Ich versuche Async-Callbacks von einer .Net-COM-DLL zu Delphi-Client .exe aufzurufen, aber diese scheint in registrierungsfreiem COM nicht ordnungsgemäß zu funktionieren, während synchrone Rückrufe funktionieren und auch async Rückrufe funktionieren, wenn es kein reg-free COM ist.Rückruf von .Net COM-DLL zu Delphi-Client in Registrierung-frei (nebeneinander) COM

Mein globaler Fall ist, dass ich eine fremde Closed-Source-.Net-DLL habe, die einige öffentliche Ereignisse aufdeckt. Ich muss diese Ereignisse an die Delphi App weitergeben. Also entschied ich mich, eine Zwischen-DLL zu erstellen, die als COM-Brücke zwischen meiner App und dieser anderen DLL funktionieren würde. Es funktionierte gut, wenn meine DLL über regasm registriert wurde, aber die Dinge werden schlimmer, wenn ich auf reg-free COM umschalte. Ich habe meinen Fall auf ein kleines reproduzierbares Beispiel verkürzt, das nicht von der anderen DLL abhängt, also werde ich es unten veröffentlichen.

Basierend auf this answer machte ich eine öffentliche Schnittstelle ICallbackHandler, die ich erwarte von Delphi Client-Anwendung zu bekommen:

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

Dann habe ich einen starken Namen zu dll gab, und registriert sie über Regasm.exe.

Jetzt wandte ich mich an Delphi-Client. Ich den TLB-Wrapper-Code erstellen Component > Import Component > Import a Type Library verwendet, die mir

gab
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; 

und erstellt einen Handler und eine gewisse Form mit zwei Tasten und Memo Dinge zu testen:

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. 

Also, ich laufe es, gedrückt ‚sync 'und' async 'Tasten und alles hat wie erwartet funktioniert. Beachten Sie, wie der Thread-IDs einer Aufgabe kommt nach ‚End asynchronem Aufruf‘ Leitung (auch mit einiger Verzögerung wegen Thread.Sleep):

all works via registration-COM

Ende des ersten Teils. Jetzt habe ich auf die Verwendung von R Registration-free (Side-by-Side) COM umgestellt. Basierend auf this answer fügte ich dependentAssembly Teil meiner Delphi App-Manifest:

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

die mt.exe tool ich mit einem Manifest für meine dll generiert:

<?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> 

Dann unregistriert ich die dll und die App laufen. Und ich fand, dass nur synchrone Teile der Rückrufe arbeiten:

enter image description here

Edit: Beachten Sie, dass Sie mit /tlb Option abzumelden müssen, sonst wird es weiterhin auf dem lokalen Rechner arbeiten, als ob dll war immer noch registriert (see).

Ich habe eine Reihe von Dingen schon müde, und ich bin mir nicht sicher, was ich als nächstes tun soll. Ich habe den Verdacht, dass der ursprüngliche Ansatz überhaupt nicht funktionieren sollte und ich ein paar Threads auf der Delphi-App-Seite implementieren muss. Aber ich bin mir nicht sicher was und wie. Jede Hilfe wäre willkommen!

Antwort

6

Sie müssen die Schnittstelle ICallbackHandler registrieren.Also, in der gleichen Datei, wo Sie haben das clrClass Element, sondern als Geschwister der file Elemente hinzufügen:

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

Dies teilt COM einen externen Proxy/Stub zu verwenden, die Typenbibliothek Marshaler ({00020424- 0000-0000-C000-000000000046}) und teilt dem Typbibliotheks-Marshaler mit, nach Ihrer Typbibliothek zu suchen ({XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}). Diese GUID ist die GUID Ihrer Assembly, die Sie in den Projekteigenschaften finden (überprüfen Sie AssemblyInfo.cs).

Sie müssen diese Typbibliothek generieren. Da Sie registrierungsfreie COM möchten, denke ich, TLBEXP.EXE passt die Rechnung perfekt, können Sie es als ein Post-Build-Ereignis einrichten.

Schließlich können Sie eine separate Typbibliotheksdatei behalten oder Sie können sie in Ihre Assembly einbetten. Ich rate Ihnen, es getrennt zu halten, noch mehr, wenn Ihre Montage groß ist.

In beiden Fällen müssen Sie dies in das Manifest einfügen. Hier ist ein Beispiel eine separate TLB-Datei mit:

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

Wenn Sie die Typbibliothek einbetten, fügen Sie die folgenden als Kind des <file name="ComDLLNet.dll"/> Element:

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

Die Registrierung des Proxy ist das Richtige, aber @Mikhail sollte auch 'this.handler.Callback (GetThreadInfo())' nicht direkt aus einem Pool-Thread aufrufen, ohne COM-Marshalling. Es sei denn, seine Delphi-Seite erwartet den Rückruf in einem zufälligen Thread. – Noseratio

+0

Ich habe versucht, 'comInterfaceExternalProxyStub' -Element hinzuzufügen, aber auf das .dll-Element selbst zu verweisen. Jetzt habe ich Ihren Vorschlag mit seperate 'Datei' Element für .tlb versucht und es hat nichts geändert, aber scheint angemessener, so werde ich es behalten. – Mikhail

+0

@Noseratio: das scheint tatsächlich der Fall zu sein. Könnten Sie mir bitte eine Anleitung für die richtige Umsetzung des Marshalling geben? – Mikhail

1

Dies ist zu lang einen Kommentar zu sein , also posten Sie es als Antwort.

Ein Zeiger auf COM-Schnittstellen sollten nie von eine andere COM-Wohnung ohne richtige Marshalling zugegriffen werden. In diesem Fall ist this.handler (höchstwahrscheinlich) ein STA COM-Objekt, das in einem Delphi-STA-Thread erstellt wurde. Dann wird es direkt von einem .NET-MTA-Pool-Thread-Thread innerhalb Task.Run aufgerufen, ohne irgendeine Art von COM-Marshalling. Dies ist ein Verstoß gegen die harten Regeln von COM, die hier beschrieben sind INFO: Descriptions and Workings of OLE Threading Models.

Dasselbe gilt für einen verwalteten RCW-Proxy, der eine COM-Schnittstelle auf der .NET-Seite umschließt. Der RCW wird nur den Methodenaufruf von verwaltetem zu nicht verwaltetem Code marshallen, aber es wird nichts über COM-Marshalling tun.

Dies kann zu allen Arten von bösen Überraschungen führen, besonders wenn das OP auf die Benutzeroberfläche der Delphi-App innerhalb handler.Callback zugreift.

Jetzt ist es möglich, dass das Objekt handler den Free Threaded Marshaler aggregiert (dies hätte seinen eigenen rules to follow, und ich bezweifle, dass es der Fall mit dem OP-Code ist). Sei es so, der Zeiger auf handler Objekt wird in der Tat zu dem gleichen Zeiger vom FTM abmarschiert. Der Servercode, der das Objekt von einem anderen Thread (Task.Run(() => { ... this.handler.Callback(GetThreadInfo() ...})) aufruft, sollte das COM-Objekt jedoch nie Frei-Thread annehmen, und es sollte weiterhin das richtige Marshalling durchführen. Wenn Glück, wird der direkte Zeiger zurückgegeben, wenn unmarshaling.

eine Reihe von Methoden Es gibt die Marshalling zu tun:...

  • CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream
  • CoMarshalInterface/CoUnmarshalInterface
  • Global Interface Table (GIT)
  • CreateObjrefMoniker/BindMoniker.
  • usw.

Natürlich, für die oben genannten Methoden Serialisieren zu arbeiten, der richtige COM-Proxy/Stub-Klassen sollten über eine Side-by-Side-Manifest, registriert oder bereitgestellt werden, wie Paulo Madeira's answer erklärt.

Alternativ kann eine benutzerdefinierte dispinterface verwendet werden (in diesem Fall würden alle Aufrufe IDispatch mit OLE Automation marshaler gehen), oder jede andere Standard-COM-Schnittstelle, die dem Standard-COM-Marshaller bekannt ist. Ich verwende oft IOleCommandTarget für einfache Rückrufe, es erfordert nichts, um registriert zu werden.

+0

Haben Sie tatsächlich versucht, ein RCW (zum eigentlichen Objekt oder einen Proxy/Stub) in einer Wohnung zu erwerben und in einer anderen Wohnung zu nutzen? Ich habe mir nie darum gekümmert, und [dieses Blog-Post] (http://blogs.msdn.com/b/mbend/archive/2007/04/18/the-mapping-between-interface-pointers-and-runtime -callable-wrappers-rcws.aspx) ist ziemlich explizit darüber, wie viel Backstage-Arbeit ein RCW tut. – acelent

+0

@PauloMadeira, habe ich viele Male. Das Aufrufen einer Methode auf demselben RCW über Apartments hinweg ist so gut wie das Aufrufen einer Methode für den zugrunde liegenden Raw-COM-Schnittstellenzeiger über Apartments hinweg. Das heißt, RCW ist in dieser Hinsicht transparent, und der Rest hängt wirklich von dem bestimmten COM-Objekt ab. Sie * können * in Ordnung sein, wenn Sie sicher wissen, dass das Objekt korrekt als Frei-Thread implementiert ist (was hier sicher nicht der Fall ist). – Noseratio