Erstellung eines DMX Ausgabeplugins in C#/C++ - Info

  • Hallo zusammen,


    nachdem man meiner Meinung nach viel zu wenige Informationen zum Thema Pluginentwicklung für DMXControl 2.xx in anderen Sprachen als VB6.0 hier im Forum und im Wiki findet, trag ich hier mal zusammen, was ich die letzten Tage so recherchiert und rausgefunden habe (teilweise durch Trial&Error).


    Meine Ausgangslage war, dass ich ein DMX Interface auf der Arduino Plattform entwickelt und erfolgreich zum Laufen gebrachte hatte, und nun dafür ein Ausgabeplugin für DMXControl schreiben wollte. Schnell war klar, dass hier sämtliche Samples und Informationen auf Visual Basic 6.0 hinauslaufen, nachdem auch DMXControl selbst in VB6 geschrieben ist. Das kam für mich allerdings nicht infrage, da ich erstens aus Prinzip (aufgrund persönlicher Vorlieben) vermeide, irgendetwas in BASIC Syntax zu schreiben, und auch nicht irgendwo ein altes VB6 ausgraben wollte, nachdem man als Student schon legalen Zugriff auf die neuesten Versionen von MS Visual Studio hat. Hier und da findet man zwar auch ein paar Anregungen und Anfragen für Plugins in C++ oder .NET, aber wirklich was konkretes war nicht dabei. Auch die Wrapper Lösung (DMXC -> VB6 -> .NET) gefiel mir nicht so besonders.


    Zuerst muss man einmal wissen, dass VB6 die letzte Version von VB war, die nicht auf .NET basierte, sondern dessen komplette Objektorientierung über COM/ActiveX Objekte geregelt wurde. Will man also ein Ausgabeplugin für DMXControl schreiben, muss dies konkret dann eine COM/ActiveX-DLL sein, die bestimmte Schnittstellen implementiert (unabhängig von VB6). Deswegen ist die Aussage, dass man Plugins in jeder Sprache schreiben kann, in der man DLL's erzeugen kann, schon einmal grenzwertig. Dies ist zwar prinzipiell immer möglich, stellt aber einen nicht zu realisierenden Aufwand dar, falls die entsprechende Entwicklungsumgebung keine Vorlagen und Konstrukte zur Erzeugung von COM/ActiveX Objekten bereitstellt. In meinem Fall kamen jetzt nur C# (oder jede andere .NET Sprache) und C++ (hier allerdings konkret auch mit MS Visual Studio, sonst kann mans wohl auch so ziemlich vergessen) infrage. Nach etwas Überlegung habe ich mich dann für C++ entschieden.


    Nachfolgend noch ein paar Ausführungen, wie DMXControl konkret ein Ausgabeplugin instantiiert (bzw. allgemein COM-Objekte instantiiert werden). Bitte beachten, dass ich keinen Source Code kenne, sondern nur durch die Verhaltensweise, wie ich sie beobachten konnte, beschreiben kann:

    • - Grundsätzlich läuft der Aufruf eines COM-Objektes etwas komplizierter als der einer normalen DLL Funktion. Es reicht also nicht der Pfad zur DLL und der Name der Funktion dazu, sondern es gehört weit aus mehr dazu.
    • - Konkret wird ein COM/ActiveX Objekt normalerweise immer durch den Aufruf der WindowsAPI-Funktion CoCreateInstance(Ex) instantiiert. Um ein ActiveX-Objekt zu instantiieren, braucht man entweder die ClassID (CLSID, eine eindeutige GUID zur Identifikation der COM/ActiveX Klasse) oder die ProgID (ein freundlicher Name wie z.B. "SampleDMXPlugin.clsOutputDriver"), welcher dann wiederum auf die ClassID verweist). Beide finden sich in der Registry (HKEY_CLASSES_ROOT(/CLSID)), und im Schlüssel "InprocServer32" unter der CLSID verweist dann tatsächlich wieder ein Pfad auf die DLL.
    • - Wie kommen die Informationen in die Registry? Klassische ActiveX/COM-Objekte besitzen eine (native) DLL-Funktion DllRegisterServer() die man entweder auf der Kommandozeile mit regsvr32 oder direkt aufrufen kann. Diese Funktion sorgt dafür, dass sämtliche Registry-Einträge erstellt werden, das ActiveX/COM-Objekt wird "registriert". Dies geschieht entweder einmal bei einer Installation durch einen Installer oder wie im Falle von DMXControl bei jedem Programmstart erneut. Dies kann Vorteile haben (die Existenz und Korrektheit der Registry-Einträge ist immer sichergestellt), andererseits auch Nachteile (siehe dann unter C#/.NET). Dies wird v.a. dann gemacht, wenn das konkrete Objekt nur durch ein einziges Programm (hier nur DMXControl) verwendet wird, sodass es dieses am Progammende durch DllUnregisterServer() die Regsistrierung auch wieder entfernen kann.
    • - Woher weiß DMXControl aber dann, welche CLSID bzw. ProgID es instantiieren soll, die Anzahl und v.a. welche Plugins sind ja zur Entwicklungszeit nicht bekannt? Zu jedem ActiveX/COM-Objekt gibt es eine sogenannte Type Library. Dies ist eine binäre Datei, in der die Schnittstelle, die konkreten Objekte/Klassen, Methoden und Eigenschaften des ActiveX/COM-Objektes definiert sind. Diese gibt es oft separat als .tlb-Datei und wird auch benötigt, um sich z.B. für ein fremdes ActiveX/COM-Objekt automatisch von Visual Studio in C++ Wrapper-Funktionen generieren lassen kann, um ActiveX/COM-Objekte wie nativen Code aufrufen zu können. In fertigen ActiveX/COM-DLL's wird diese meistens auch als Ressource namens "TYPELIB" in der DLL selbst mit ausgeliefert. Diese verwendet DMXControl, um den Librarynamen und den CoClass-Namen herauszufinden, welche dann als ProgID verwendet werden, welche zur CLSID führt, um die ActiveX/COM-Klasse zu instantiieren.
    • - Wenn man also den "Weg" beschreibt, der beschritten wird, beginnt dieser bei der DLL selbst (wo auch sonst?), nimmt dann einige Umwege über die Registry, und landet dann letztendlich doch wieder bei ihr selbst. Ob das ganze im Allgemeinen so sinnvoll ist, sei mal dahingestellt, aber so funktioniert nun mal COM/ActiveX.
    • - Microsoft hat einen immensen Aufwand getrieben, um den Übergang von COM/ActiveX zu .NET möglichst einfach zu machen (Stichwort COM-Interop). Es ist also generell möglich von .NET aus COM-Objekte zu verwenden, als auch umgekehrt (wie hier im Falle von DMXControl benötigt). Das ist ja auch mit eine Grundidee des Konzeptes von COM, dass es egal ist, was unter der Haube steckt, solange das Interface bekannt ist, kann mans aufrufen. Also spielt die Tatsache, dass das ganze am Schluss in Managed Code endet, für DMXControl eigentlich keine Rolle, weil es nichts davon mitbekommt.
    • - Es ist also grundsätzlich möglich, ein Plugin für DMXControl direkt in .NET zu schreiben (auch ohne Wrapper). Man erstellt ein Interface und eine konkrete Implementation und muss noch ein paar kleinere Details beachten. In den Projekteinstellungen von Visual Studio . Zwei kleinere Sachen habe ich dabei allerdings festgestellt, die für mich das Ausschlaggebende waren, das ganze nicht in .NET weiter zu realisieren:
    • - Die Type Library wird standardmäßig nicht als TYPELIB Ressource in eine mit C# erstellte Klassenbibliothek-DLL eingebunden. So hat DMXControl erst mal keine Möglichkeit rauszufinden, welche ActiveX/COM-Klasse es zu instantiieren hat, das verrät die DLL ansonsten nämlich nicht. Man kann sich allerdings von Visual Studio die .tlb-Datei generieren und diese dann den Linker mithilfe eines Ressource-Scripts wieder an die DLL anhängen lassen oder auch nachträglich mithilfe eines Ressourceneditors hinzufügen. Daran scheiterts also nicht.
    • - In .NET geschriebene ActiveX/COM-Interop-Klassen werden im Allgemeinen nicht über die native in der DLL selbst ansässige Funktion DllRegisterServer() registriert, sondern durch ein externes Tool namens RegAsm. Hier auch der Nachteil davon, dass DMXControl bei jedem Programmstart alle Plugins registriert. Man kann natürlich die DLL außerhalb von DMXControl selbst mit RegAsm registrieren, diese ist dann auch ganz regulär mit ClassID oder ProgID instantiierbar. Es wird aber in jedem Fall in einer Fehlermeldung seitens DMXControl enden, wenn es versucht DLLRegisterServer() aufzurufen. Ob DMXControl dann sofort abbricht, hab ich nicht probiert, wenn es trotzdem versucht, dann die Klasse zu instantiieren, würde dies theoretisch funktionieren. Für den, der damit leben kann, dass DMXControl bei Programmstart immer eine Fehlermeldung ausgibt, wär das dann eine akzeptable Lösung.
    • - Mit den Microsoft Foundation Classes (MFC) und der Active Template Library (ATL) gibt es zwei grundsätzlich zwei Möglichkeiten, in MS Visual C++ ActiveX/COM-Ojbekte zu erstellen. Da ATL jüngeren Datums und nicht so überladen wie MFC ist, und so auch kleinere Dateigrößen ermöglicht, habe ich mich für ATL entschieden.


    Short Guide zur Erstellung eines DMXControl Ausgabeplugins auf Basis C++/ATL:

    • - Anlegen eines neuen ATL-Projektes in Visual Studio, im Assistenten "Anwendungstyp: DLL" auswählen und "Zusammenführung von Proxy/Stub-Code" ankreuzen; Name des Projektes nicht zu lang wählen, da später das Eingabefeld für die ProgID in der Länge begrenzt ist!
    • - Projekt -> Klasse hinzufügen -> Einfaches ATL-Objekt

    - Für "Kurzer Name", "Klasse" und "Coclass" den Namen "clsOutputDriver" eingeben (bei "Klasse" auf das automatich von VS hinzugefügte C am Anfang achten -> weg damit!)
    - Für "Schnittstelle" (Interface-Name) "_clsOutputDriver" eingeben
    - Als ProgID folgendes eingeben: [NamedesProjektes]Lib.clsOutputDriver (Das "Lib" hängt VS bei der Definition in der IDL Datei an, aus der dann die Type Library generiert wird).
    - Keine Unterstützung für Dateityp-Handler (nicht benötigt)
    - Optionen sollten sollten so belassen werden wie sie sind (wichtig ist v.a. das Schnittstelle: Dual!)
    - Fertigstellen

    • - In der Klassenansicht durch Rechtsklick auf das "_clsOutputDriver" Interface -> Hinzufügen -> "Methode hinzufügen" nacheinander folgende Methoden definieren (DispID spielt normalerweise keine Rolle):
    Code
    [id(0x60030000)] HRESULT Disable([out,retval] VARIANT_BOOL* pRetVal);
    [id(0x60030001)] HRESULT Enable([out,retval] VARIANT_BOOL* pRetVal);
    [id(0x60030002)] HRESULT PluginName([out,retval] BSTR* pRetVal);
    [id(0x60030003)] HRESULT SetChannel([in,out] long* Channel, [in,out] long* value, [out,retval] VARIANT_BOOL* pRetVal);
    [id(0x60030004)] HRESULT Configure([out,retval] VARIANT_BOOL* pRetVal);
    [id(0x60030005)] HRESULT Init([in,out] BSTR* CallingAppTitle, [in,out] BSTR* MyFilename, [in,out] IDispatch** DMXCPluginHelper, [out,retval] VARIANT_BOOL* pRetVal);
    [id(0x60030006)] HRESULT Term([out,retval] VARIANT_BOOL* pRetVal);


    • - Die Methoden werden dann von VS in die IDL-Datei übernommen (aus der dann später auch die TypeLibrary und C++ Wrapper generiert werden)
    • - In der Datei clsOutputDriver.cpp fügt VS automatisch für die einzelnen Methoden Funktionsrümpfe hinzu, in der dann die konkrete Implementierung realisiert werden kann. Und schon hat man in Null komma Nix ein Ausgabeplugin in C++ kreiert. Die erstellte DLL in das DMXControl Verzeichnis kopieren und in [Name].out.dll umbenennen.


    Anmerkungen:

    • - In der Form hat man ein reines Ausgabeplugin erstellt. Die DMX Kanalwerte werden bei jeder Änderung seitens DMXControl durch Aufruf von SetChannel() mitgeteilt.
    • - Falls man auch DMX Input will, müsste man noch einen Schritt weiter gehen. In der Init() Funktion bekommt man einen Zeiger auf eine Instanz des DMXCPluginHelpers mitgeliefert. Mit einer entsprechenden Type Library (.tlb-Datei), in der die Klasse zu dem Objekt definiert ist, sollte man theoretisch in der Lage sein, durch die #import-Direktive sich C++ Wrapper (.tlh) für ActiveX/COM-Aufrufe auf das Objekt generieren zu lassen. Damit sollte man die entsprechenden Methoden des Objekts zum Setzen der Kanalwerte aus dem Plugin heraus aufrufen können. So weit bin ich allerdings nicht gegangen, da nicht benötigt.
    • - Bei Aufruf der Configure()-Methode will man wahrscheinlich ein Dialogfeld zum Einstellen von diversen Plugin-Optionen darstellen. Hierzu bietet sich ein ATL-Dialogfeld an, das auch durch Projekt->Klasse hinzufügen erstellt werden kann. Dies lässt sich dann ganz bequem im Ressourcendesigner gestalten, und die entsprechende Funktionalität kann wieder sehr leicht hinzugefügt werden, da VS sämtliche Funktionsrümpfe wieder automatisch generiert, die dann nur noch gefüllt werden müssen.


    Ich hoffe, das kann dem einen oder anderen etwas helfen, ist leider ein etwas längerer Forenbeitrag geworden. Vielleicht kann man es etwas abgeändert auch ins Wiki stellen.
    Gruß

  • Hallo zusammen,


    auf Anfrage poste ich hier ein in Visual C++ (Visual Studio 2012) erstelltes ATL-Projekt, mit dem ich mein DMX Control Plugin realisiert habe. Den für mein selbstgebautes DMX Interface spezifischen Code habe ich entfernt und wo es ging den Namen und sämtliche GUID's geändert.


    Ich empfehle dennoch ein neues Plugin immer als frisches vom Visual Studio Assistenten generiertes Projekt zu beginnen, da hier sämtliche GUID's usw. neu angelegt werden und so es zu keinen Problemen kommen kann, wenn jetzt mehrere Plugins anhand dieses Beispielprojekts entstehen und diese parallel verwendet werden wollen (ansonsten gewinnt unter Umständen das letzte von DMXControl registrierte Plugin, welches die Registrierung der vorherigen Plugins überschreibt). Wer sich sicher ist kann sämtliche GUID's natürlich auch manuell ändern (bestimmte GUID's treten aber mehrmals an verschiedenen Stellen auf, die natürlich dann übereinstimmen müssen!).


    Am wichtigsten und als erster Anlaufpunkt empfohlen ist natürlich die Methode clsOutputDriver:: SetChannel in der Datei clsOutputDriver.cpp, die von DmxControl aufgerufen wird, sobald sich der Wert eines Dmx Kanals ändert. Auch im Beispielprojekt dabei ist ein kleiner About-Dialog, der als ATL-Dialog realisiert ist und von DmxControl über die clsOutputDriver::Configure Methode aufgerufen wird (eigentlich zur Konfiguration der Kommunikation mit dem Interface gedacht, für das er natürlich gerne umgebaut werden darf).


    Viel Spaß damit!


    Gruß
    dmxmaster

  • Newly created posts will remain inaccessible for others until approved by a moderator.

    The last reply was more than 365 days ago, this thread is most likely obsolete. It is recommended to create a new thread instead.