"COM Lite", Mono und GCC



  • Hallo,

    mein aktuelles Projekt (und es wird wohl nicht das Letzte seiner Art sein) ist teils in C#, teils in C++ geschrieben. Weil ich das Projekt auch unter Linux kompilieren will (mit Mono und GCC), kann ich für die Interaktion kein C++/CLI benutzen. Momentan mache ich also folgendes:

    - die "primary executable" ist eine C++-Anwendung. Unter Windows lade ich die CLR über eine C++/CLI-Assembly, unter Linux hoste ich Mono mit der dafür vorgesehenen Schnittstelle. Funktioniert beides gut.

    - für den Austausch zwischen C#- und C++-Code definiere ich mir COM-Interfaces. Da die COM-Infrastruktur unter Linux nicht zur Verfügung steht, habe ich die nötigsten Dinge selbst implementiert. Ich habe also plattform- und sprachunabhängigen Code, um eine beliebige DLL zu laden und darin eine vordefinierte Funktion aufzurufen, die ein COM-Interface zurückgibt. Funktioniert auch.

    - Weil ich meinen C++-Code auch mit GCC kompiliere, kann ich nicht die ATL verwenden; stattdessen habe ich mir die die nötigsten Komfortfunktionen selbst implementiert (z.B. eine Implementation für IUnknown , HRESULT in Exception umwandeln und umgekehrt, Fehlerinformation setzen/abfragen mit Set/GetErrorInfo() , BSTR-Handling, Annotationsmechanismus für IIDs à la __uuidof )). Auch das funktioniert.

    Das Problem ist, daß ich die ganzen Schnittstellen jetzt mehrfach deklarieren muß, einmal in C# und einmal in C++. Wenn ich z.B. folgende Interface-Definition in Pseudo-C# habe:

    [Guid ("4A0A38C5-4059-47A4-91F0-C72CC8920939")]
    public interface IService
    {
        Guid GetServiceID ();
    }
    
    [Guid ("0625A9CC-DFAF-4BAA-90EC-BC2304E6020C")]
    public interface IConfigurationReader : IService
    {
        bool KeyExists (string section, string key);
        string GetValue (string section, string key);
    }
    

    Dann muß ich dafür folgende C#-Definitionen schreiben:

    [ComImport]
    [InterfaceType (ComInterfaceType.InterfaceIsIUnknown)]
    [Guid ("4A0A38C5-4059-47A4-91F0-C72CC8920939")]
    public interface IService
    {
        [MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        Guid GetServiceID ();
    }
    
    [ComImport]
    [InterfaceType (ComInterfaceType.InterfaceIsIUnknown)]
    [Guid ("0625A9CC-DFAF-4BAA-90EC-BC2304E6020C")]
    public interface IConfigurationReader : IService
    {
        // redefine methods inherited from IService
        [MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        Guid GetServiceID ();
    
        // IConfigurationReader methods
        [MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        bool KeyExists (string section, string key);
        [MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
        string GetValue (string section, string key);
        [MethodImpl (MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
    }
    

    Das ist so häßlich, weil man in Mono für jede Methode dieses furchtbare MethodImpl -Attribut angeben muß, sonst generiert der JIT-Compiler Blödsinn für Interfacemethoden (cf. http://www.mono-project.com/COM_Interop), und weil C# keine Interface-Vererbung unterstützt, so daß man die Methoden des Basisinterfaces neudefinieren muß.

    Für C++ schreibe ich folgendes:

    class UCL_UUIDCLASS ("{4A0A38C5-4059-47A4-91F0-C72CC8920939}", IService) : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE GetServiceID (GUID* clsid) = 0;
    
            // C++ helper methods
        GUID getServiceID (void)
        {
            GUID result;
            ucl::comCheck (GetServiceID (&result));
            return result;
        }
    };
    
    class UCL_UUIDCLASS ("{0625A9CC-DFAF-4BAA-90EC-BC2304E6020C}", IConfigurationReader) : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE KeyExists (BSTR section, BSTR key, BOOL* result) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetValue (BSTR section, BSTR key, BSTR* result) = 0;
    
            // C++ helper methods
        bool keyExists (ucl::NativeStringArg section, ucl::NativeStringArg key)
        {
            BOOL result;
            ucl::comCheck (KeyExists (ucl::nativeToCOMString (section), ucl::nativeToCOMString (key), &result));
            return result != 0;
        }
        ucl::NativeString getValue (ucl::NativeStringArg section, ucl::NativeStringArg key)
        {
            ucl::COMString result;
            ucl::comCheck (GetValue (ucl::nativeToCOMString (section), ucl::nativeToCOMString (key), result.getRef ()));
            return result.asString ();
        }
    };
    

    ucl::NativeString ist ein Typedef für std::string oder std::wstring , je nach Plattform. BSTRs verwenden auch unter Linux mit Mono UTF-16. Die "helper methods" sind nur dazu da, mir beim Aufruf Boilerplate-Code zu ersparen.

    Man sieht, das ist eine ganze Menge Schreibarbeit, und zu allem Überfluß kommt alles doppelt vor, einmal in C# und einmal in C++. Also suche ich nach Wegen, mir diese Interface-Definitionen automatisch generieren zu lassen.

    Der übliche Weg unter Windows wäre, sich eine Typbibliothek zu erstellen und diese dann nach Belieben zu importieren. Das ist schön bequem, und es gibt guten Tool-Support. Um das für GCC/Mono/Linux zu adaptieren, müßte ich einen Weg finden, COM-Interop-Assemblies in Mono zu unterstützen, und ich müßte GCC dazu bringen, die Headerdatei zu kompilieren, die VC++ bei einem #import -Statement generiert.

    Alternativ könnte ich meine Interfaces ebenfalls in IDL definieren, aber dann MIDL nehmen, um einen Header zu erzeugen. Der ist fast frei von VC++-Spezifika, sollte also mit GCC verwendbar sein. Für C# müßte ich immer noch irgendwie eine Typbibliotheks-Assembly erstellen und Mono-kompatibel machen.

    Eine weitere Möglichkeit wäre, mein obiges Pseudo-C# als Interfacedefinition zu nehmen, mit NRefactory zu parsen und daraus die gewünschten Definitionen für C++ und C# (oder beliebige weitere Sprachen) zu erzeugen. Konzeptuell gefällt mir das am besten, aber es klingt nach viel Arbeit und, noch schlimmer, nach Neuerfinden des Rads. Ist ja nicht so, als wäre ich der erste mit dem Problem 😞

    Auf der Mono-Seite wird außerdem auf dieses nunmehr seit sechs Jahren unberührte Perl-Skript hingewiesen, das in ähnlicher Weise aus XPIDL-Dateien C#-Definitionen macht. Das könnte ich nehmen und anpassen, wobei ich nicht sicher bin, ob die Arbeit mit diesem Regex-Parser-Hack leichter ist als mit NRefactory.

    Wie mache ich das nun am besten?



  • Gibt es einen Grund weshalb du kein C-Interface benutzt? Das ist um Längen einfacher und könnte auch weniger Aufwand bedeuten.



  • gcc schrieb:

    Gibt es einen Grund weshalb du kein C-Interface benutzt? Das ist um Längen einfacher

    Ist das so?

    Klar, P/Invoke wird in Mono etwas direkter unterstützt als COM Interop. Aber dann muß ich für jede einzelne Funktion, die ich in C++ exportiere, einen Plain-C-Wrapper schreiben:

    extern "C" HRESULT UCL_DLLEXPORT STDMETHODCALLTYPE ConfigurationReader_KeyExists (void* self,
        const ucl::NativeChar* section, const ucl::NativeChar* key, BOOL* result)
    {
        return static_cast<ConfigurationReader*> (self)->KeyExists (section, key, result);
    }
    

    Für jede exportierte Funktion muß ich einen P/Invoke-Import schreiben, bei dem bereits feststehen muß, aus welcher DLL die Funktion kommt:

    [DllImport("MyNativeLib.dll", CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)]
    uint ConfigurationReader_KeyExists (IntPtr self, string section, string key, [MarshalAs (UnmanagedType.U4)] out bool result);
    

    Für die Rückgabe von Strings muß ich jetzt so ein bescheuertes fixed-buffer-Pattern verwenden wie bei den meisten WinAPI-Funktionen (nächstbestes Beispiel: GetComputerName()). Und um Fehlerbehandlung in C# muß ich mich selber kümmern, wohingegen bei COM-Interfaces automatisch Exceptions geworfen werden, wenn der HRESULT-Rückgabewert != 0 ist (deshalb brauchte ich oben für C++ die "helper methods", die man in C# gratis dazubekommt). Außerdem muß ich die Speicherverwaltung der Objektreferenzen selbst machen, was bei COM-Interfaces auch die CLR übernimmt.

    Außerdem habe ich ein zentrales Stück Code in C#, das eine Konfigurationsdatei einliest, in der steht, wie die DLLs heißen, die eine Implementation für ein bestimmtes Interface bereitstellen. D.h., ich weiß zur Übersetzungszeit noch nicht, aus welcher DLL ich die Funktion importieren muß. Also muß ich selber mit GetProcAddress() bzw. dladdr() und Marshal.GetDelegateForFunctionPointer() herumhantieren, was natürlich nochmal mehr Spaß macht als die ganzen P/Invoke-Deklarationen.

    Weiter kommt hinzu, daß Code, der mit Interfaces hantiert, anstatt sich selbst um seine Abhängigkeiten zu kümmern, viel leichter systematisch zu testen ist. Ich kann ein Interface, das normalerweise in C++ implementiert ist, einfach durch ein in C# geschriebenes (oder ein von meinem Testframework automatisch generiertes) Mockup ersetzen und muß dafür nichts am Code ändern. Wenn der Code selbst P/Invoke benutzt, um auf die Implementierung zuzugreifen, ist das nicht möglich.

    Kurzum: von "um Längen einfacher" oder "weniger Aufwand" kann nicht die Rede sein 😞



  • Wieviel spielt denn Performance eine Rolle? Ist das der Grund, warum du über COM o.ä. gehen willst? Was wäre mit Corba oder lokaler WebService? Da hättest du schon Tools/Frameworks für beide Plattformen.



  • Es läuft also darauf hinaus dass du die Interface-Definitionen 1x schreiben willst, und daraus dann die anderen nötigen Files generieren.

    Die Frage ist nur noch: in welchem Format schreibst du die Interface-Definitionen?

    Die "Pseudo-C# als Interfacedefinition" Variante würde mir nicht so gefallen, weil verwirrend. Die Pseudo-C# Klassen sehen dabei dann ja so aus als wären sie der verwendete Source-Code, sind es aber nicht. Hmmm...

    Ich würde mal gucken ob es fertige MIDL Parser (idealerweise in C#) gibt die du als Basis verwenden kannst.
    Ausgehend von einem fertigen MIDL-AST einen Code-Generator zu schreiben der das von dir benötigte MIDL Subset "kann" sollte ja nicht so viel Aufwand sein.

    Oder, andere Idee: Guck dir mal die von der MS Toolchain erzeugten COM-Interop-Assemblies an. Vielleicht kannst du aus denen über Reflection die nötigen Infos rausziehen um die Mono-kompatiblen Interface-Definitionen zu erzeugen.

    und weil C# keine Interface-Vererbung unterstützt, so daß man die Methoden des Basisinterfaces neudefinieren muß.

    Huch?



  • Artchi schrieb:

    Wieviel spielt denn Performance eine Rolle?

    Ich möchte schon gerne das UI auf der einen und das Model auf der anderen Seite haben können, oder die Implementierung einer Skriptfunktion auf der einen und das Skript auf der anderen. Einen Webservice oder Marshaling über Pipes kommt mir da suboptimal vor. Außerdem besteht mein Problem, wie hustbaer richtig erkannt hat, hauptsächlich in der möglichst automatischen Generierung der Interface-Definitionen in mehreren Sprachen. Das wird auch nicht leichter, wenn ich jetzt einen lokalen Webservice statt COM verwende.

    Über CORBA weiß ich leider nichts; wenn das mein Problem löst, könntest du mir mehr darüber sagen wie?

    hustbaer schrieb:

    Es läuft also darauf hinaus dass du die Interface-Definitionen 1x schreiben willst, und daraus dann die anderen nötigen Files generieren.

    Genau. Ich habe mit der oben beschriebenen Technik jetzt ein Projekt umgesetzt, das mittlerweile gut läuft, aber die parallele Wartung der Interfaces war a) nervig und b) eine lästige Fehlerquelle, die schon zahlreiche Stunden gekostet hat, z.B., weil das Marshaling manchmal unerwartetes Standardverhalten hat, oder weil ich eine Methode vergessen habe einzufügen. Unter Mono/Linux gab es noch lustigere Artefakte. (Mono/Windows habe ich aufgegeben, nachdem ich gemerkt hatte, daß Mono das Exception-Handling in C++ kaputtmacht, indem es einen kaputten SEH-Filter registriert.) Das würde ich mir in Zukunft gerne ersparen, und noch wichtiger: wenn jemand anderes an einem solchen Projekt arbeitet, der diese Fehler noch nicht gemacht hat, braucht er nicht auch nochmal auf die Nase fallen.

    hustbaer schrieb:

    Die Frage ist nur noch: in welchem Format schreibst du die Interface-Definitionen?

    Die "Pseudo-C# als Interfacedefinition" Variante würde mir nicht so gefallen, weil verwirrend. Die Pseudo-C# Klassen sehen dabei dann ja so aus als wären sie der verwendete Source-Code, sind es aber nicht. Hmmm...

    Ja, das stimmt. Der Vorteil wäre, daß ich sie in Visual Studio schreiben könnte, was sehr bequem ist, aber der Nachteil, wie du sagst, daß ich dann eine Pseudoassembly habe, die gar nicht zur Laufzeit benötigt wird.

    hustbaer schrieb:

    Ich würde mal gucken ob es fertige MIDL Parser (idealerweise in C#) gibt die du als Basis verwenden kannst.

    Hmjagut, nur ist IDL praktisch genau so verbose wie C++, vielleicht noch ein bißchen schlimmer 😞

    hustbaer schrieb:

    Oder, andere Idee: Guck dir mal die von der MS Toolchain erzeugten COM-Interop-Assemblies an.

    Habe ich mal gemacht. Ich könnte ja, wie im Eingangspost skizziert, MIDL nehmen, um einen C++-Header und via TLBIMP eine Typbibliothek für .NET zu bekommen. Der Reflector sagt mir, daß solche Interop-Assemblies abhängigkeitsfrei ist; ich darf nur nicht die Compiler-Magic zum Erzeugen eines COM-Objekts verwenden, denn der Compiler generiert dafür einen Aufruf von Activator.CreateInstance() , und das wird von Mono nicht implementiert. Möglicherweise kann ich die also in Mono direkt verwenden.

    Die von MIDL erzeugten Header sind halt nicht so hübsch, u.a., weil ich auf die helper methods verzichten muß und die auch nicht mit einem einfachen Postprocessing-Schritt hinzufügen kann 😞

    hustbaer schrieb:

    und weil C# keine Interface-Vererbung unterstützt, so daß man die Methoden des Basisinterfaces neudefinieren muß.

    Huch?

    Siehe z.B. hier und hier. In C# besagt "interface inheritance" lediglich, daß du von Interface IDerived zu Interface IBase casten kannst. In COM sagt "interface inheritance" darüber hinaus, daß die VMT von IDerived zuerst die Einträge von IBase , dann seine eigenen Einträge enthält, d.h. jede IDerived -Referenz ist auch ohne Typecast eine IBase -Referenz. Und zumindest in meinem Ansatz führte das dazu, daß ich in C# die Funktionen von IBase in IDerived wiederholen mußte, wenn ich ein kompatibles VMT-Layout bekommen wollte.

    Wenn das nicht so wäre, könnte ich ja auch den umgekehrten Weg gehen, also: eine Assembly voller Interfaces in C# als ComVisible definieren, irgendwie eine IDL rausziehen, mit MIDL einen C++-Header erstellen. Das wäre dann etwas bequemer, als selbst IDL zu schreiben. Aber so muß ich selbst Duplikate der geerbten Funktionen schreiben, um "interface inheritance" à la COM zu simulieren. Wenn ich aber eine C++-Typbibliothek nach .NET importiere, generiert TLBIMP diese Duplikate automatisch für mich.

    Danke jedenfalls für eure Beschäftigung mit dem Thema, das hat mir schon viel geholfen.


Anmelden zum Antworten