Entwickler-Ecke

Dateizugriff - Auch eine Frage zu Plugins im Programm


Henry - Fr 10.10.03 15:20
Titel: Auch eine Frage zu Plugins im Programm
Hallo,

ich habe begonnen in eines meiner Programme eine Pluginschnitstelle einzubauen. Nun habe ich in meinem Programmverzeichnis einen Ordner \Plugin in den die Plugins (DLLs) vom User hineinkopiert werden sollen. Soweit ist es auch kein Problem, aber ich möchte die Möglichkeit schaffen das sich jeder User nur die Plugins integrieren kann die er gerne nutzen möchte. Im HTML-Editor Phase5 wird ein Menüeintrag eingefügt wenn eine neue DLL in den Pluginordner kopiert wurde.

So ungefähr würde ich das auch gerne lösen, denn sonst müste ich ja zu jedem neuen Plugin eine neue Version der Software erstellen damit der eintrag vorhanden ist. Ich weiß nur leider nicht wie ich es machen kann das immer wenn eine neue DLL im Ordner hinzugefügt wird ein neuer Eintrag in meinem Menü auftaucht und dann auch noch die Funktion in der entsprechenden DLL aufruft.

Kann mir da jemand helfen?

Danke im voraus

Moderiert von user profile iconTino: Überflüssige Absätze entfernt.


bIce - Fr 10.10.03 16:50

Ich vestehe nicht ganz wo das Problem ist. Alle PlugIn-Dlls müssen die gleiche Schnittstelle haben.
Bsp:

Delphi-Quelltext
1:
2:
3:
4:
function Init: boolean; stdcall;
function GetName(var name: string) : Boolean; stdcall;
function GetIcon(var icon: TIcon) : Boolean; stdcall;
function OnCall(event: Integer): Boolean; stdcall;

Zurückgegeben wird ob die Funktion erfolgreich ausgeführt wurde oder nicht.

Dann in der Hauptanwendung musst Du beim Laden, alle Dlls laden. Den Namen und das Icon abholen und daraus ein Menü erstellen. Beim Schließen alle Dlls wieder freigeben.

Du musst Noch z.B. ein Array mit HMODULE erstellen, wo die Handles zu den Dlls gespeichert werden. und den Index an TMenuItem.Tag zuweisen, dann eine Ereignisbehandlungsroutine für OnClick erstellen, wo mit Hilfe des TMenuItem.Tag und dem Array auf OnCall zugegriffen wird. Wie man Dlls lädt/freigibt, und funktionen benutzt muss ich wohl nicht erklären. Wenn du es nicht weißt dann schaue in der Hilfe nach LoadLibrary/FreeLibrary/GetProcAddress

Moderiert von user profile iconTino: Delphi-Tags hinzugefügt.


Henry - Fr 10.10.03 19:16

Danke Dir erstmal für die Antwort.

Das Die schnittstelle bei allen DLLs gleich sein muß ist klar.
Mein Problem besteht darin, das Menü zu erstellen.

Wie kann ich denn eine unbekannte Anzahl an DLLs laden von denen ich den Dateinamen nicht kenne?
Dann ist mir auch nicht ganz klar wie ich mit dem die Menüeinträge erstelle und das OnClick- ereignis für die einzelnen Einträge definiere.

Für die ersten Versuche habe ich mit folgender Funktion die DLL dynamisch eingebunden


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
Procedure DLLAufruf(Dateiname, Funktionsname : String);
 VAR Plugin    : TPlugin1;
     FunctPtr  : TFarProc;
     DLLHandle : THandle;

  Begin
   DLLHandle := LoadLibrary(PChar(Dateiname));
   FunctPtr  := GetProcAddress(DLLHandle, PChar(Funktionsname));
   if FunctPtr <> nil then
    Begin
     @Plugin := FunctPtr;
     Plugin; //Aufruf der DLL-Funktion. Mit Übergabe der Parameter
     FunctPtr := nil;
    End
     Else
       Showmessage('Plugin nicht gefunden!');
       FreeLibrary(DLLHandle);
 End;


bIce - Fr 10.10.03 23:04

Um an den Dateinamen zu kommen musst Du wissen, wo die Dll gespeichert ist. Dann kannst Du FindFirst/FindNext/FindClose-Verwenden um die Namen zu erhalten. Um sich ganz sicher zu sein, dass es sich dabei um ein PlugIn handelt musst Du die dll laden, und versuchen alle notwendigen Funktionen abzufragen. Wenn es ein PlugIn ist dann wird es im Array gespeichert.
Der Übersichtslichkeitshalber habe ich mich nur auf das Notwendigste beschränkt. Ich habe es nicht auf irgendwelche Fehler geprüft, falls etwas nicht funktionieren sollte, dann lass es mich wissen.

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
83:
84:
85:
86:
87:
88:
89:
90:
91:
92:
93:
94:
95:
96:
97:
98:
99:
100:
101:
102:
103:
104:
105:
106:
107:
108:
// Speichert alle Pfade zu Dlls, die gleichzeitig PlugIns sind
// Path ist der Pfad wo sich die Plugins befinden müssen
procedure GetPlugInNames(path: Stringconst items : TStringList);
var
   sr : TSearchRec;
begin
   path := IncludeTrailingBackslash(path)
   if FindFirst(path + '*.dll', faAnyFile, sr) = 0 then begin
      repeat
         if IsPlugIn(path + sr.Name) then
            items.Add(path +sr.Name);
      until FindNext(sr) <> 0;
      FindClose(sr);
    end;
end;

// Prüft ob es sich bei der Dll um ein PlugIn handelt
function IsPlugIn(path: String): Boolean;
var
   h : HMODULE;
   name: TGetNameFunc; 
   evt : TOnCallFunc; 
begin
   Result := false;
   h := LoadLibrary(PChar(path));
   if h = nil then exit;
   name := TGetNameFunc(GetProcAddress(h, 'GetName'));
   if name = nil then exit;
   evt := TOnCallFunc(GetProcAdress(h, 'OnCall'));
   if evt = nil then exit;
   
   Result := true;
end;

// Klasse des Fensters das die PlugIns verwaltet
TForm1 = class(TForm)
...
private
   // Hier ein statisches Array, dass die PlugIns verwaltet.
   plugIns_ : array[0..1024of HMODULE;
   count_ : Integer; // Anzahl der PlugIns.
   // Ereignisbehandlung für die Menüs, der PlugIns
   class procedure OnPlugInMenuClick(sender : TObject);
   // Lädt alle PlugIns
   procedure LoadAllPlugIns;
   // Gibt alle PlugIns frei
   procedure FreeAllPlugIns;
end;

// Ereignisbehandlung für die Menüs, der PlugIns
class procedure TForm1.OnPlugInMenuClick(sender : TObject);
var
      evt : TOnCallFunc;
begin
   if sender is TMenuItem then
      with sender as TMenuItem do
         if (Tag >= 0and (Tag < count_) then begin
            evt := TOnCallFunc(GetProcAddress(plugIns_[Tag], 'OnCall'));
            if evt <> nil then
               evt;
         end;
      end;
end;

   // Lädt alle PlugIns
procedure TForm1.LoadAllPlugIns;
var
   files : TStringList;
   i : Integer;   
   nameF: TGetNameFunc;
   name : String;
   menu : TMenuItem;
begin
   count_ := 0;
   files := TStringList.Create;
   try
      GetPlugInNames(ExtractFilePath(Application.Name) + 'PlugIns', files);
      for i:=0 to files.Count - 1 do begin
         plugIns_[count_] := LoadLibrary(files.Strings[i]);

         nameF:= TInitFunc(GetProcAdress(h, 'Init'));
         if not initF(name) then
            FreeLibrary(plugIns_[count_])
         else begin
            menu := TMenuItem.Create(mnuHauptEintrag);
            menu.Caption := name;
            menu.OnClick := OnPlugInMenuClick;
            menu.Tag := count_;
            mnuHauptEintrag.Items.Add(menu);
         end;
         Inc(count_);
      end;
   finally
      files.Free;
   end;
end;

// Gibt alle PlugIns frei
// dabei beschränkt sich die Prozedur nur auf das Freigeben der Dlls, 
// und nicht das Löschen der Menüs
procedure TForm1.FreeAllPlugIns;
var 
   i : Integer;
begin
   for i := 0 to count_ - 1 do
      FreeLibrary(plugIns_[i]);
   count_ := 0;
end;


Henry - Fr 10.10.03 23:11

Danke Dir.

Ich werde mich mal damit ein wenig beschäftigen.
Melde mich nochmal falls ich noch Probleme habe.


Henry - Sa 11.10.03 23:51

Hallo,

Ich bin noch dabei mich mit den Code auseinanderzusetzen und mir eine eigene Unit zu entwickeln.
Dein Codebeispeil hilft mir übrigens sehr dabei.
Nun bin ich auf folgendes Problem gestoßen.

Die Procedure wo ich mir meine Menüeinträge für die Plugins erzeuge sieht an der Problemstelle so aus:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
For i := 0 to Items.Count - 1 do
  Begin
   DLLHandle     := LoadLibrary(PChar(Items.Strings[i]));
   GetPluginName := TGetPluginName(GetProcAddress(DLLHandle,'PluginName'));

   NewItem         := TMenuItem.Create(nil);
   Newitem.Caption := GetPluginname;
   NewItem.Tag     := Anzahl;
   NewItem.OnClick := PluginsOnClick;
   PluginMenu.Add(NewItem);
   Inc(anzahl);
  End;//For...


Wenn ich aber mein Projekt Compilieren möchte kommt an der Stelle

Delphi-Quelltext
1:
  NewItem.OnClick := PluginsOnClick;                    

die Fehlermeldung:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur

Kommentiere ich die Stelle aus, funktioniert alles wie gewünscht, nützt nur nichts einen Eintrag zu erzeugen der nichts aufruft:)
Wie kann ich denn im Code oben meine Procedure aufrufen die bei OnClick aufgerufen werden soll? Was mache ich falsch? Irgendwie muß es doch möglich sein, das ich eine Procedure oder Funcktion aufrufen kann, trotzdem ich den Menüeintrag erst zur Laufzeit erzeuge.

Könnte mir da nochmal jemand auf die Sprünge helfen?

Danke im voraus


bIce - So 12.10.03 00:01

Henry hat folgendes geschrieben:

Wenn ich aber mein Projekt Compilieren möchte kommt an der Stelle

Delphi-Quelltext
1:
  NewItem.OnClick := PluginsOnClick;                    

die Fehlermeldung:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur

Kommentiere ich die Stelle aus, funktioniert alles wie gewünscht, nützt nur nichts einen Eintrag zu erzeugen der nichts aufruft:)

Schreibe vor der Definition/Deklaration von PluginsOnClick "class" (Ohne Anführungsstriche). Oder siehe den Quelltext von mir. Ich habe ihn angepasst.


Henry - So 12.10.03 00:06

Danke,

ich werde es mal versuchen


Henry - So 12.10.03 00:35

Wenn ich einfach Class davor schreibe, dann kommt folgende Fehlermeldung

[Fehler] Plugin.pas(17): Deklaration von 'PluginsOnClick' unterscheidet sich von vorheriger Deklaration

Kann es daran liegen, das ich alle meine Funktionen die ich für meine Pluginbearbeitung verwenden möchte in einer eigenen Unit habe und nicht wie Du in der Unit des Formulars?
Denn ich möchte der Übersicht halber eine eigene Unit dafür haben, die alles enthält was ich brauche, dann kann ich es einfacher in andere Anwendungen übernehmen und muß nicht immer bei null anfangen.

Die Declaration sieht nun bei mir so aus

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
  Interface

   ...

  Class Procedure PluginsOnClick(Sender: TObject);
  Procedure LoadPlugins(Pfad: String; Pluginmenu:TMenuItem);



und die Procedure so:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
Class Procedure PluginsOnClick(Sender: TObject);
Begin
 If Sender is TMenuItem then
  Begin
   With Sender as TmenuItem do
    Begin
     
    End;
  End;

End;


Wie gesagt alles in einer eigenen Unit, falls es einen Unterschied machen sollte.


maximus - So 12.10.03 11:42

IMAO ist das mit dem 'class' quatsch...wüsste nicht was das zum problem beiträgt?


Quelltext
1:
[Fehler] Plugin.pas(49): Inkompatible Typen: Methodenzeiger und reguläre Prozedur                    


Das bedeutet, dass du 'PluginsOnClick' als methode in einer klasse definieren musst:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
Procedure TDeineKlasse.PluginsOnClick(Sender: TObject); 
Begin 
 If Sender is TMenuItem then 
  Begin 
   With Sender as TmenuItem do 
    Begin 
      
    End
  End

End;


dann kannst du dein event endlich zuweisen :wink:


bIce - So 12.10.03 13:40

maximus hat folgendes geschrieben:
IMAO ist das mit dem 'class' quatsch...wüsste nicht was das zum problem beiträgt?

Stimmt, weiß garnicht mehr wie ich darauf gekomment bin ?;)
class macht es nur zu einer statischen Methode, was gar nicht erforderlich ist.
[code]


Zitat:

Das bedeutet, dass du 'PluginsOnClick' als methode in einer klasse definieren musst:

Delphi-Quelltext
1:
Procedure TDeineKlasse.PluginsOnClick(Sender: TObject);                    


So stand es schon seit längerem da. Ist Henry vielleicht nicht aufgefallen.


Henry - So 12.10.03 21:50

Hallo ich bin es wieder :)

Ich bin mit eurer Hilfe schon ein großes Stück weiter gekommen.

Es ist aber ein weiteres Problem aufgetreten.

Ich möchte einfach eine Funktion in der PluginDLL aufrufen die mir ein True zurück giebt
wenn es ein Plugin ist.
Wenn es aber keine PluginDLL ist, dann existiert die Function ja nicht, deshalb bekomme ich eine Fehlermeldung. Giebt es eine Möglichkeit festzustellen ob die funktion existiert? So wie:

Delphi-Quelltext
1:
 FileExists('../datei.dll')                    

So das das Programm nicht versucht die Funtion aufzurufen und dann abstürtzt.

So siet die betreffende Stelle bei mir aus. Nur wenn die Function in der DLL nicht existiert, dann giebt es einen Absturz des Programmes.

Delphi-Quelltext
1:
2:
  GetPluginName := TGetPluginName(GetProcAddress(Plugins_[Count_],'PluginName'));
  Init := TInit(GetProcAddress(Plugins_[Count_],'Init'));

Vieleicht könntet Ihr mir da nochmal unter die Arme greifen


Henry - Di 14.10.03 20:15

Das Problem ist gelöst, danke allen die mir geholfen haben.

EDIT: Meine Lösung die ich nun nutze ist:,

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
If GetProcAddress(Plugins_[Count_],'PluginName') = NIL then
 Begin
  // Wenn Function nicht Vorhanden
 End
  Else
   Begin
    // Ereignis wenn Function vorhanden
   End;

Falls jemand anders auch soetwas sucht, und demjenigen somit auch geholfen ist.


bIce - Mi 15.10.03 18:31

Henry hat folgendes geschrieben:

[...]
So das das Programm nicht versucht die Funtion aufzurufen und dann abstürtzt.

So siet die betreffende Stelle bei mir aus. Nur wenn die Function in der DLL nicht existiert, dann giebt es einen Absturz des Programmes.
[...]

Wozu willst Du eigentlich eine Spezielle Funktion, um zu überprüfen ob es ein PlugIn ist? Es wäre doch besser wenn Du einfach prüfst ob alle anderen Erforderliche Schnittstellen vorhanden sind. So wie in der oben geposteten Funktion IsPlugIn().

Wenn Du das Modell aber in mehreren Programmen , dann könntest Du eine ID für bestimmtes Programm verwenden, diese IDs könntest Du z.B. (wie in COM) mit CoCreateGuid() erzeugen.


Henry - Mi 15.10.03 19:27

bIce hat folgendes geschrieben:

[...]
Wozu willst Du eigentlich eine Spezielle Funktion, um zu überprüfen ob es ein PlugIn ist? Es wäre doch besser wenn Du einfach prüfst ob alle anderen Erforderliche Schnittstellen vorhanden sind. So wie in der oben geposteten Funktion IsPlugIn().
[...]

Da hast Du recht, letzendlich werde ich es wohl so machen.

bIce hat folgendes geschrieben:

Wenn Du das Modell aber in mehreren Programmen , dann könntest Du eine ID für bestimmtes Programm verwenden, diese IDs könntest Du z.B. (wie in COM) mit CoCreateGuid() erzeugen.

Mit einer Guid habe ich mich auch noch nicht befasst, aber ist denn Die nicht bei jedem erzeugen anders?
Ich werde aber mal in der Hilfe nachschauen.
Da ich versuche meine Pluginschnittstelle erweiterbar zu gestallten, werde ich erstmal eine erste Version zum laufen bringen. Dann werde ich das ganze immer weiter ausbauen. Dazu sind natürlich alle Tips die ich bekommen kann nützlich für mich.
So lernt man ja auch dazu.


Knulli - Fr 23.03.07 14:54

Hallo Forum,

ist zwar schon ne Weile her, das hier diskutiert wurde, möchte trotzdem mal sehen, ob der Thread "noch lebt".

Frage: Könnte man in der (PlugIn-)DLL nicht auch eine einzige Funktion nur schreiben, die eine Referenz auf ein TMyPlugIn-Objekt zurückgibt? Alle eigentlich benötigten Funktionalitäten wären dann über die Member (Methoden und Eigenschaften) der Klasse "erreichbar". Stellt sich natürlich die Frage, was mit Methoden/Eigenschaften ist, die neu hinzukommen. Was steckt eigentlich alles an Informationen in so einem Parameter bzw. Rückgabewert vom Typ "TMyPlugIn"? Nur der Klassenname oder auch Informationen über alle Member? Hab mal irgendwann was über Late-Binding gehört, wo erst zur Laufzeit ermittelt wird, ob es ein bestimmtes Member in einer Klasse gibt. Wie kann ich sowas in Delphi erzwingen? Wenn das gehen würde, hätte man ja mit "TMyPlugIn" eine wunderbare, unendlich erweiterbare PlugIn-Schnittstelle.

Ich hoffe, nichts durcheinandergerbracht zu haben und hoffe auf Meinungen/Ideen/Kritik.

mfg Knulli


HelgeLange - Fr 23.03.07 18:18

rein technisch gesehen kannst du ein Standard-Object-Typ erzeugen und ihm mit Dispatch selbst-definierte Nachrichten schicken. Regiert das Object drauf (liefert ein bestimmtes Ergebnis zurück), wird diese Funktion von dem Object unterstützt. So kannst Du Dir eine Feature-Matrix für jedes geladene Plugin erstellen.

Das Problem wird sein, dass Du immer hinterher-hinkst. Gerade, wenn Du von anderen Programmierern Plugins akzeptieren willst. Solche Plugins haben einfach den Nachteil, sehr starr zu sein, da sie immer vom Code des Loaders abhängig sind und wie er die Schnittstelle interpretiert. Und je mehr du einbaust in die Schnittstelle (je mehr Funktionen), desto komplizierter wird das "Flöhe-Hüten" ;)

Schau doch mal in den Thread "ERP Framework Components" (in "Kommerzielle Projekte"), dort habe ich ein wenig beschrieben, wie ich das gelöst habe, vielleicht hilft es Dir etwas ? Ansonsten frag halt hier nochmal :)