Neu vorgestellt, Programmierung

Designer Plugin-Schnittstelle

Plugin

Die aktuelle Version von CONZEPT 16 erweitert den Designer um die Plugin-Schnittstelle. Der nachfolgende Artikel beschreibt, um was es sich dabei handelt und wie die Schnittstelle funktioniert.

Einleitung

Vermutlich kennen Sie den Begriff ‚Plugins‘ bereits aus anderen Softwareprodukten. So können Anwender den Firefox-Browser z. B. um existierende Plugins erweitern, damit bestimmte Dateitypen angezeigt oder abgespielt werden (wie z. B. Shockwave Flash-Dateien). Software-Entwickler kennen Plugins aus der Eclipse-Entwicklungsumgebung, die sich auch um Plugins erweitern lässt. Auf diese Art lässt sich beispielsweise ein Versionskontrollsystem in Eclipse integrieren. Ein Plugin erweitert also die Funktionalität einer bestehenden Software, ohne dass diese hierzu geändert werden muss. Stattdessen verwendet das Plugin eine (meist herstellerabhängige) API um sich an die bestehende Software „anzustöpseln“.

Plugin-Funktionalität im Designer

Mit Freigabe der Version 5.7.10 stellt der CONZEPT 16-Designer ebenfalls eine Plugin-Funktionalität zu Verfügung. Die Kommunikation zwischen Designer und Plugin geschieht hierbei über eine TCP/IP Socket-Verbindung. Hierzu baut der Designer während des Startvorgangs eine passive Socket-Verbindung auf. Die hierzu notwendige Portnummer wird in der folgenden Reihenfolge ermittelt:

  • Kommandozeile
    Die Portnummer ist auf der Kommandozeile des Client bzw. Advanced-Client angegeben.
    Beispiel: c16_apgi.exe * MyArea MyUser MyPassword /C16PluginPort=4715
  • Konfigurationsdatei
    Die Portnummer ist in der Konfigurationsdatei über den Eintrag PluginPort hinterlegt. Beim Starten des Clients ist dies die Datei c16.cfg, beim Advanced-Client entsprechend c16_apgi.cfg.
  • Umgebungsvariable
    Die Portnummer ist als Umgebungsvariable C16PluginPort unter Windows gesetzt.

Ohne Angabe der Portnummer an einer dieser 3 Stellen oder bei expliziter Angabe der Portnummer 0, startet der Designer ohne Plugin-Funktionalität.
Damit der Designer mit Plugin-Funktionalität startet, ist außerdem das Vorhandensein eines Plugin-Kennwortes notwendig (Abb. 1). Dieses wird für die Authentifizierung der Plugin-Anwendung herangezogen.

Plugin-KennwortAbb. 1: Plugin-Kennwort

Die Angabe muss bei dem Benutzer erfolgen, der im Designer angemeldet ist. Ein leeres Kennwort führt dazu, dass der Designer ohne Plugin-Funktionalität startet.

Anwendungsprotokoll

Um verifizieren zu können, ob eine bestimmte Designer-Instanz mit Plugin-Funktionalität läuft oder nicht, kann im Anwendungsprotokoll nachgeschaut werden. Dieses kann über den Menüpunkt Extras/Anwendungsprotokoll öffnen angezeigt werden. Bei erfolgreicher Einrichtung enthält dieses den folgenden Eintrag:
Plugin manager init : area=… / user=… / port=… / Successfully initialized
Hat die Einrichtung nicht geklappt, wird ein Eintrag mit Angabe des Fehlerwertes generiert.

Überprüfung im Designer

Ob die Plugin-Funktionalität aktiviert wurde, können Sie auch ganz schnell über die Titelleiste im Designer ablesen (Abb. 2).

Portnummer im DesignerAbb. 2: Portnummer im Designer

Bei aktivierter Plugin-Funktionalität wird die Portnummer im Titel angezeigt.

Plugin-Anwendung
Schema Plugin-AnwendungAbb. 3: Plugin-Anwendung

Eine Plugin-Anwendung ist ein eigenständiger Prozess, welcher über Sockets mit dem Designer in Verbindung steht. Die Anwendung muss deshalb nicht zwingend mittels CONZEPT 16 entwickelt werden. Prinzipiell kann die Entwicklung in jeder Programmiersprache erfolgen, die Sockets unterstützt. Für die Erstellung einer Plugin-Anwendung mit CONZEPT 16 stehen jedoch APIs bereit, die die Entwicklung vereinfachen (Abb. 3). Die CONZEPT 16-Core und Converter API stellen dem Entwickler die Funktionalität bereit, damit der Entwicklungsprozess einer CONZEPT 16 Plugin-Anwendung möglichst einfach und standardisiert erfolgen kann.

CONZEPT 16 – Core API

Die Funktionen dieser Schnittstelle sind in der Prozedur Plugin.Core.Inc deklariert. Die Implementation der Funktionen ist in der Prozedur Plugin.Core enthalten. Beide Prozeduren, die CONZEPT 16-Converter API sowie ein kleines Beispiel sind Bestandteil der CodeLibrary, die mit der Version 5.7.10 freigegeben wurde.

Plugin-Instanz

Kernelement einer Plugin-Anwendung bildet die Plugin-Instanz (im Folgenden kurz als Instanz bezeichnet). Diese hat im Wesentlichen zwei Aufgaben:

  • Einrichtung und Verwaltung der, für die Kommunikation mit dem Designer notwendigen, Socket-Verbindung.
  • Thread für den Empfang von Nachrichten, die vom Designer an die Plugin-Anwendung gesendet werden.

Eine Instanz wird mit der API-Funktion Plugin.Core:InstanceNew angelegt.

declare Plugin.Core:InstanceNew
(
  aPluginPort  : word;    // Plug-in port to connect to.
  opt aTimeout : int;     // Timeout (connect/read/write) in milliseconds.
  opt aFrame   : handle;  // Frame to receive EvtJob (Client only).
)
: int;

Der Funktion wird die vom Designer verwendete Portnummer übergeben. Daneben besitzt sie zwei weitere optionale Argumente.

  • aTimeout
    Eine Zeitspanne in Millisekunden, welche angibt, wie lange auf die Verbindungsherstellung gewartet werden soll. Die Zeitspanne wird auch beim Lesen und Schreiben auf den Socket verwendet.
  • aFrame
    Deskriptor eines Frame-Objektes. Bei neuen Nachrichten wird dort das Ereignis EvtJob ausgelöst. Der Frame-Deskriptor wird ignoriert, wenn die Funktion aus dem SOA-Service aufgerufen wird.

Der Rückgabewert der Funktion ist eine fortlaufende Instanz-Nummer, die für den Aufruf der weiteren API-Funktionen benötigt wird. Im Fehlerfall liefert die Funktion einen entsprechenden Fehlerwert. Die Fehlerwerte sind in der Prozedur Plugin.Core.Inc definiert.

Die Plugin-Anwendung kann aus bis zu 10 Instanzen bestehen. Das Erstellen einer zusätzlichen Instanz kann z. B. sinnvoll sein, wenn die Anwendung mit mehr als einer Designer-Instanz kommunizieren soll oder mehrere Plugin-Funktionalitäten in der Anwendung abgebildet werden sollen.

Wird eine Instanz nicht mehr benötigt, kann diese durch die Funktion InstanceClose geschlossen werden.

declare Plugin.Core:InstanceClose
(
  aID : int;          // Plug-in instance ID to close.
);

Im Argument aID wird die zuvor generierte Instanz-Nummer übergeben. Alternativ können einfach auch alle Instanzen geschlossen werden. Hierfür hält die API die Funktion InstanceCloseAll bereit.

declare Plugin.Core:InstanceCloseAll
();
CONZEPT 16 – Converter API

Die Converter-API besteht aus den Prozeduren Plugin.Converter.Inc und Plugin.Converter. Sie enthält Funktionen für die Authentifizierung und die Kommunikation mit dem Designer.

Authentifizierung

Nachdem eine Instanz erstellt wurde, sendet der Designer einen Befehl, der die Plugin-Anwendung zur Authentifikation veranlasst. Diese muss innerhalb von 5 Sekunden auf diesen Befehl antworten. Ist die Authentifizierung nicht erfolgreich oder antwortet die Plugin-Anwendung nicht, wird die Verbindung vom Designer terminiert. Mit der Funktion Plugin.Converter:ReceiveAuth wartet die Anwendung auf die Aufforderung.

declare Plugin.Converter:ReceiveAuth
(
  aReceiverType    : int;    // (in)  Type of receiver.
  aReceiver        : int;    // (in)  Job control object / instance id.
  var aSerial      : int64;  // (out) Serial number.
  var aUser        : alpha;  // (out) User name
  opt aWaitTimeout : int;    // (in)  Maximum wait time in milli seconds.
)
: int;                       // Error code.
  • aReceiverType und aReceiver
    Im Argument aReceiver kann der Deskriptor eines JobControl-Objektes oder einer Instanz-Nummer übergeben werden. Die Unterscheidung, um was es sich handelt ergibt sich über das Argument aReceiverType. Bei Angabe von _ReceiverByInstanceID wird eine Instanz-Nummer erwartet. Bei Angabe von _ReceiverByJobControl wird der Deskriptor eines JobControl-Objektes erwartet. Die Angabe von _ReceiverByJobControl macht vor allem im Ereignis EvtJob Sinn, da dort der Deskriptor des JobControl-Objektes bekannt ist. Die Instanz-Nummer ist dort normaler Weise nicht vorhanden.
  • aSerial
    In diesem Argument wird eine eindeutige, fortlaufende Nummer (die Antwortkennung) zurückgegeben, die im weiteren Verlauf der Authentifizierung benötigt wird.
  • aUser
    Hier wird der Name des Benutzers zurückgeliefert, für den die Authentifizierung angefordert wird. Dabei handelt es sich um den Benutzer, der auf der Designer-Seite angemeldet ist.
  • aWaitTimeout
    Über dieses optionale Argument kann angegeben werden, wie lange die Plugin-Anwendung auf die Aufforderung des Designers warten soll, bevor ein Timeout zurückgegeben wird.

Wurde die Aufforderung des Designers erfolgreich empfangen, dann liefert die Funktion _ErrOK, sonst einen Fehlerwert. Die Anwendung antwortet nun mit folgender Funktion.

declare Plugin.Converter:ReplyAuth
(
  aInstanceID      : int;       // (in) Instance ID.
  aSerial          : int64;     // (in) Serial number.
  aPluginName      : alpha(40); // (in) Plugin name.
  var aPassword    : alpha;     // (in) password (C16 charset).
  opt aWaitTimeout : int;       // (in)  Maximum wait time in milli seconds.
)
: int;
  • aInstanceID
    Hier muss die Instanz-Nummer, die von Plugin.Core:InstanceNew zurückgegeben wurde, übergeben werden.
  • aSerial
    In diesem Argument wird die zuvor erhaltene Antwortkennung übergeben.
  • aPluginName
    Der Name der Plugin-Anwendung. Das Argument darf nicht leer sein und nicht nur aus Leerzeichen bestehen. Dieser Name wird im Anwendungsprotokoll ausgegeben, für Einträge, welche die Plugin-Schnittstelle betreffen.
  • aPassword
    Plugin-Kennwort für den zuvor empfangenen Benutzer.
  • aWaitTimeout
    Über dieses optionale Argument kann angegeben werden, wie lange die Plugin-Anwendung auf die Antwort des Designers warten soll, bevor ein Timeout zurückgegeben wird.

Wenn die Authentifizierung erfolgreich war, liefert die Funktion den Rückgabewert _ErrOK. Schlug die Authentifizierung dagegen fehl, wird der Rückgabewert _ErrRights zurückgegeben.

Befehle und Ereignisse

Nachdem sich die Plugin-Anwendung erfolgreich am Designer angemeldet hat, kann sie die eigentliche Plugin-Funktionalität nutzen.

  • Ereignisse empfangen
    Der Designer sendet bei Aktionen des Anwenders Ereignisse, die von der Plugin-Anwendung empfangen und verarbeitet werden können, z. B. beim Übersetzen einer Prozedur im Editor.
  • Befehle senden
    Die Plugin-Anwendung kann Befehle an den Designer senden, damit dieser bestimmte Aktionen durchführt, wie z. B. das Öffnen einer Prozedur im Editor.

Befehle und Ereignisse werden über einen Name gekennzeichnet. So hat der Befehl zum Öffnen einer Prozedur den Name ‚Designer.Editor.Open‘.
Des Weiteren muss der Befehl wissen, welche Prozedur geöffnet werden soll. Befehle können deshalb Argumente enthalten. Bei Ereignissen ist dies ganz ähnlich. Wird eine Prozedur im Editor geöffnet, sendet der Designer das Ereignis ‚Designer.Editor.OpenDone‘. Der Name der betreffenden Prozedur ist ebenfalls in einem Argument enthalten.

Befehle senden

Das folgende Beispiel demonstriert, wie mit Hilfe der Converter-API ein Befehl generiert und an den Designer gesendet werden kann.

sub OpenProcedure
(
  aInstanceID     : int;
  aValue          : alpha;
)

  local
  {
    tPluginCmd    : handle;
  }

{
  // Create plugin command.
  tPluginCmd # Plugin.Converter:CreateCmd(sPluginCmdKindCmd, 'Designer.Editor.Open');
  if (tPluginCmd > 0)
  {
    // Add procedure name and type as arguments.
    tPluginCmd->Plugin.Converter:AddArgStr('Name', aValue, sPluginArgStrC16);
    tPluginCmd->Plugin.Converter:AddArgInt('Type', 0);

    // Send command to designer.
    Plugin.Converter:SendCmd(aInstanceID, tPluginCmd);

    // Delete command.
    tPluginCmd->Plugin.Converter:DeleteCmd();
  }
}

Das Beispiel implementiert eine Funktion, welche eine Prozedur im Editor des Designers öffnet. Hierzu wird mit der Funktion Plugin.Converter:CreateCmd ein Befehl angelegt. Als Argument wird sPluginCmdKindCmd übergeben sowie der Name des Befehls. War der Aufruf der Funktion erfolgreich, liefert sie einen Deskriptor auf ein PluginCommand. Diesem können Argumente zugeordnet werden. Im Beispiel werden zwei Argumente hinzugefügt.

  • Name
    Dieses Argument kennzeichnet den Name der zu öffnenden Prozedur. Da es sich um eine Zeichenkette vom Typ alpha handelt, wird die Funktion Plugin.Converter:AddArgStr aufgerufen. Im letzten Argument wird die Zeichenkodierung der Zeichenkette übergeben. Mit sPluginArgStrC16 wird definiert, dass der Prozedurname, der in aValue angegeben ist im CONZEPT 16-Zeichensatz kodiert ist.
  • Type
    Dieses Argument gibt an, ob eine Prozedur (Type = 0) oder ein Text (Type = 1) geöffnet werden soll. Da es sich um einen numerischen Wert vom Typ int handelt, muss die Funktion Plugin.Converter:AddArgInt zur Übergabe verwendet werden.

Der vollständig definierte Befehl wird anschließend mit der Funktion Plugin.Converter:SendCmd an den Designer geschickt. Die Instanz-Nummer (Plugin.Core:InstanceNew) wird im ersten Argument übergeben, das PluginCommand im zweiten.

Zum Schluss wird das mit Plugin.Converter:InstanceNew erstellte Kommando wieder gelöscht. Das erledigt die Funktion Plugin.Converter:DeleteCmd.

Ereignisse empfangen

Analog zum Senden von Befehlen funktioniert das Empfangen von Ereignissen. Das folgende Beispiel reagiert darauf, wenn im Designer eine Prozedur geöffnet wurde.
In diesem Fall sendet der Designer der Plugin-Anwendung das Ereignis ‚Designer.Editor.OpenDone‘.

sub EvtJob
(
  aEvt                  : event;        // Ereignis
  aJobCtrlHdl           : handle;       // Job-Kontroll-Objekt
)
: logic;

  local
  {
    tInstanceID         : int;
    tPluginCmd          : handle;
    tResult             : int;
    tProcName           : alpha;
  }

{
  tPluginCmd # Plugin.Converter:CreateCmd();
  tResult # Plugin.Converter:ReceiveCmd(_ReceiverByJobControl,
                                        aJobCtrlHdl,
                                        tPluginCmd,
                                        var tInstanceID);

  if (tResult = _ErrPluginCoreThreadTerm)
  {
    // Verbindung zum Designer beendet.
    ...
  }
  else if (tResult = _ErrOK and
           Plugin.Converter:IsCmdKindEvt(tPluginCmd))
  {
    if (tPluginCmd->Plugin.Converter:IsCmdName('Designer.Editor.OpenDone'))
    {
      tPluginCmd->Plugin.Converter:GetArgStr('Name',var tProcName);

      // tProcName verwenden.
      ...
    }
  }

  // Delete command.
  tPluginCmd->Plugin.Converter:DeleteCmd();

  return(true);
}

Zunächst wird ein PluginCommand erstellt. Das geschieht auch wieder über die Funktion Plugin.Converter:CreateCmd. Im Unterschied zum Senden bleibt das Kommando hier jedoch leer und wird erst durch den Aufruf von Plugin.Converter:ReceiveCmd gefüllt. Nach erfolgreicher Durchführung der Funktion enthält tPluginCmd das Ereignis und tInstanceID die Instanz-Nummer, an die das Ereignis gesendet wurde.

Liefert Plugin.Converter:ReceiveCmd den Rückgabewert _ErrPluginCoreThreadTerm, wurde die Verbindung vom Designer beendet. Dies geschieht, wenn der Anwender den Designer beendet.

Im Falle von _ErrOK liegt ein Ereignis oder ein Befehl vor und tPluginCmd enthält den Name und die Argumente (sofern vorhanden).
Das Beispiel überprüft mit der Funktion Plugin.Converter:IsCmdKindEvt ob es sich bei dem Inhalt von tPluginCmd um ein Ereignis handelt. Sofern dies der Fall ist, prüft das Beispiel mit Plugin.Converter:IsCmdName, ob es sich um das Ereignis ‚Designer.Editor.OpenDone‘ handelt. Wenn auch dies der Fall ist wird der Name der geöffneten Prozedur über die Funktion Plugin.Converter:GetArgStr ermittelt.

Am Ende wird das Kommando durch Plugin.Converter:DeleteCmd wieder freigegeben.

Abschließende Anmerkungen

Dieser Artikel soll die Grundlagen der Plugin-Entwicklung vorstellen. Der nächste Artikel zu diesem Thema wird die Informationen aus diesem Artikel vertiefen und anhand der Implementation einer Plugin-Anwendung die praktische Seite der Plugin-Entwicklung darstellen.

6 Kommentare

6 Kommentare “Designer Plugin-Schnittstelle”

  1. @Kilian
    Man darf dabei aber nicht vergessen, dass das Schreiben eines Plugins in CONZEPT 16 einfacher umzusetzen ist, als die Implementation in einer anderen Sprache. Einfach, weil die entsprechenden APIs (Core- und Converter-API) vorhanden sind.

  2. @Michael
    Aha, danke für die Info.

    Wenn nun das Plugin allerdings nicht in C16 geschrieben werden soll, z.B. weil bereits vorhandene Funktionalität anderer Systeme genutzt werden soll, dann ist dieser Trick nicht so einfach umzusetzen, oder? Entweder man schreibt doch ein C16-Plugin, das seinerseits die externe Funktionalität nutzt oder aber umgekehrt man fügt dem externen Plugin eine Verbindung zur C16-Datenbank hinzu. Beides verkompliziert die Sache natürlich "ein wenig" 🙂

  3. @Kilian
    Danke für die Anregungen :O).
    Das Plugin-Beispiel in der CodeLibrary macht sich den Vorteil zu nutze, dass es in derselben Datenbank ausgeführt wird, in der auch der Anwender arbeitet. Dadurch lassen sich einige Dinge realisieren, die die Plugin-Schnittstelle in der aktuellen Version nicht anbietet.

    Die Liste der Dialoge wird so z.B. einfach über ein StoList-Objekt dargestellt und muss nicht per Plugin-Funktionalität ermittelt werden. Ähnlich wäre dies auch für die Reformatierung des Prozedurtextes möglich. Das hat den Vorteil, dass die Prozedur nicht übertragen werden müsste, sondern direkt aus der Datenbank gelesen werden kann. Die Anzeige kann dann wieder über die Plugin-Schnittstelle durchgeführt werden (Designer.Editor.Open).

    Wir lassen die Vorschläge in jedem Fall in die weitere Entwicklung der Plugin-Schnittstelle einfließen.

  4. @Andreas
    Danke für den Hinweis auf die Doku.

    Habe das Beispiel in der Code-Library gerade mal ausprobiert. Ziemlich coole Sache 🙂

    Derzeit sind anscheinend einfach mehr oder weniger alle über die Menüs erreichbaren Befehle und einige Ein- und Ausstiegsereignisse vorhanden. Einige darüber hinaus gehende Anwendungsfälle, die ich seit dem ersten Auftauchen der Plugin-Idee im Kopf habe, wären:
    – verhindern, dass eine Prozedur kompiliert wird (z.B. weil sie nicht einem vorgegebenen Standard entspricht), dafür bräuchte man aber wohl ein CompileStart-Ereignis und einen Befehl, der das Kompilieren verhindert
    – den kompletten Prozedurtext nach bestimmten Standards automatisch reformatieren, dafür bräuchte man die Möglichkeit, den gesamten Prozedurtext an ein Plugin zu übergeben und wieder von dort zu übernehmen
    – xml-Repräsentation(en) eines oder mehrerer Dialoge in ein Plugin einlesen und durchsuchen, k.A. ob man hierfür die Befehle Designer.Menu.File.Export.Exec und ..Import.Exec verwenden könnte, schön wäre es sicherlich, wenn der xml-Text direkt an das Plugin übergeben werden könnte

  5. @Kilian
    Es freut uns, dass Sie die Schnittstelle ausprobieren möchten.
    In der Dokumentation gibt es ein separates Kapitel für die Plugin-Schnittstelle. Hier werden alle Ereignisse und Befehle erläutert.

  6. Das ist eine sehr gute Idee. Eine Plugin-Funktionalität für den Designer war ja schon vor längerer Zeit im Blog angekündigt und schon damals habe ich mich darauf und darüber gefreut 🙂

    Ich werde diese neuen Möglichkeiten bei nächster Gelegenheit gern ausprobieren.

    Eine Liste mit den verfügbaren Befehlen/Ereignissen des Designers, die über die Plugin-Schnittstelle ausführbar bzw. verarbeitbar sind, liegt irgendwo vor, nehme ich an, oder?

Kommentar abgeben