Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Sonstige Fragen zu Delphi (https://www.delphipraxis.net/19-sonstige-fragen-zu-delphi/)
-   -   Delphi HowTo: Runtime-Packages (https://www.delphipraxis.net/171762-howto-runtime-packages.html)

RSE 23. Nov 2012 15:42

HowTo: Runtime-Packages
 
Da ich mich mit dem Thema Runtime-Packages gerade beschäftigt habe und dabei einige Fragen auftauchten, dessen Antworten ich mir erst zusammensuchen musste, möchte ich mich an einem HowTo versuchen, damit andere diese Antworten gesammelt finden können.

Sinnvolle Vorkenntnisse: DLLs

Bestandteile eines Package-Projekts
  • *.dpk: "Delphi Package" - Hauptquelltextdatei des Packages. Sie beginnt mit dem Schlüsselwort "package". Es gibt 2 Hauptabschnitte: "requires" und "contains". Requires enthält weitere Packages, die statisch in das Package eingebunden werden. Contains enthält die im Package enthaltenen Units.
  • *.dcp: "Delphi Compiled Package"
  • *.bpl: "Borland Package Library" - fertiges Compilat

Arten der Einbindung von Packages
Packages sind spezielle DLLs. Man kann sie auf 2 verschiedene Weisen benutzen: statisch oder dynamisch gelinkt.
Statisches Linken heißt, dass beim Laden des Moduls das verwendete Package mit geladen wird, bevor die Codeausführung beginnt. Wenn beispielsweise ein Programm (exe) ein Package statisch einbindet, dann muss das Package bei Programmstart verfügbar sein, damit es mit der exe geladen werden kann. Man kann programmtechnisch selbst nicht auf ein eventuelles Nichtvorhandensein der *.bpl-Datei reagieren - das Laden der exe bricht in diesem Fall mit einer Fehlermeldung ab.
Wird ein Package dagegen dynamisch eingebunden, dann startet das Modul ohne Verwendung des Packages. Erst im Laufe des Programmes lädt der selbstprogrammierte Programmcode das Package - vergleichbar mit dem dynamischen Laden einer DLL. Was bei DLLs mit dem Aufruf von LoadLibrary und UnloadLibrary geschieht, wird bei Packages mit LoadPackage bzw. UnloadPackage erledigt.

Statisch eingebundene Runtime-Packages
Wird ein Package statisch eingebunden, dann können die Units verwendet werden als seien sie Teil des Programms, direkt im Modul enthalten. In Packages werden statisch geladene Packages im requires-Abschnitt aufgeführt. Üblicherweise stehen hier rtl, ggf. vcl und weitere Packages je nach Inhalt des Packages. Um ein Package in einer exe statisch einzubinden, muss in den Projektoptionen auf der Seite "Packages" das Häkchen bei "Laufzeit-Packages verwenden" gesetzt werden. Alle im Programm verwendeten Units werden nun nicht mehr in die exe eingebaut (bzw. der daraus erzeugte Binärcode), falls diese Unit in einem der Packages enthalten ist, das unter dem Häkchen "Laufzeit-Packages verwenden" aufgeführt ist. Wird z.B. die Unit Forms verwendet und das Package vcl wird nicht als statisches Runtime-Package eingebunden, dann wird der verwendete Quellcode aus der Unit Forms direkt in die exe hineincompiliert. Es gibt also keinen Zwang alle voreingetragenen/verfügbaren Packages dort aufzuführen. Will man z.B. die Unit Forms in einem Package verwenden, ohne das Package statisch einzubinden (in den requires-Abschnitt aufzunehmen), dann muss man die Unit Forms und alle Units die sie verwendet (und alle, die diese Units verwenden etc. - alle implizit importierten Units) in den contains-Abschnitt aufnehmen. Dann werden all diese Units mit in das Package-Compilat (die *.bpl) reincompiliert. Allerdings führt dieses Vorgehen bei mir zu mehreren Fehlermeldungen W1025 Sprach-Feature wird nicht unterstützt...
Wird beispielsweise in der exe das eigene Package MyPackage als Runtime-Package statisch eingebunden und MyPackage bindet vcl als Runtimepackage statisch ein, dann wird vcl automatisch auch in der exe benutzt und z.B. die Unit Forms nicht noch einmal extra als Binärcode in der exe hinterlegt. Ein explizites Einbinden von vcl als Runtimepackage in der exe führt in diesem Fall zu keiner Änderung von Dateigrößen - weder der exe noch der MyPackage.bpl - eventuell beeinflusst es allerdings die Ausführungsreihenfolge der Unit-Initialisierung und -Finalisierung (ungetestet).

Dynamisch eingebundene Runtime-Packages
Bindet ein Modul Runtimepackages dynamisch ein, dann werden die Packages erst vom enthaltenen selbst programmierten Programmcode geladen. Bei entsprechender Programmierung ist das Modul also auch ohne das Package lauffähig - im Gegensatz zu statisch eingebundenen Runtime-Packages. Allerdings ist das Handling aufwendiger. Die im Package enthaltenen Units dürfen nirgends im Modul referenziert werden. Die Units des Packages dürfen also nicht in den uses-Abschnitten des Moduls auftauchen, wodurch auch kein im Package definierter Bezeichner verfügbar ist. Die Verwendung findet wie folgt statt:
Quellcodebeispiel im Modul, welches das Package verwendet:
Delphi-Quellcode:
type
  TInitModule: procedure; // muss mit dem Typ im Package übereinstimmen
var
  HPack: HModule;
  InitModule: TInitModule;
begin
  try
    HPack := LoadPackage('MyPackage.bpl');
    if HPack > 0 then
      try
        @InitModule := GetProcAddress(HPack, 'InitModule');
        if Assigned(InitModule) then
          InitModule
        else
          ShowMessage('Einsprungadresse für Prozedur InitModule nicht gefunden');
      finally
        UnloadPackage(HPack);
      end
    else
      ShowMessage('Das Laden des Packages "MyPackage.bpl" war nicht erfolgreich');
  except
    on E: EPackageError do
      ShowMessage('Fehler beim Laden des Packages "MyPackage.bpl":' + 
        sLineBreak + E.Message);
  end;
end;
Beispiel für eine Main-Unit im verwendeten Package:
Delphi-Quellcode:
unit MyPackageMain;

interface

procedure InitModule;

exports // gleiche Technik wie bei DLLs
  InitModule;

implementation

uses
  Dialogs;

procedure InitModule;
begin
  ShowMessage('Message aus dem Package');
end;

end.
Interaktion mit dynamisch geladenen Runtimepackages
Um auf in dynamisch geladenen Packages definierte Klassen zugreifen zu können, kann man zum Beispiel mit Klassenreferenzen, RTTI und registrierten Klassen (RegisterClass) arbeiten. Ich bevorzuge einen anderen Weg: Ich habe mir ein Package erstellt, welches diverse grundlegende Klassen definiert und globale Variablen hält. Dieses Package wird sowohl im Basismodul als auch im dynamisch eingebundenen Runtimepackage (MyPackage) als statisches Runtimepackage eingebunden. Nennen wir es BasePackage. Somit stehen sowohl im Basismodul als auch in MyPackage alle Typinformationen zur Verfügung. Selbst die globalen Variablen sind die selben, da BasePackage nur einmal "instanziert" wird (oder wie man das bei Units auch immer nennt) - die Speicheradresse einer getesteten in BasePackage definierten globalen Variable, sowie natürlich auch deren Inhalt, war aus Sicht von Basismodul und MyPackage gleich. Auch die Initialization- und Finalization-Abschnitte in BasePackage wurden über die gesamte Programmlaufzeit korrekterweise nur einmal ausgeführt.

Projektorganisation und Pfadeinstellungen
Ich habe mein Programm mit allen eigenen Packages in einer Projektgruppe zusammengefasst. Achtung: Mit dem Befehl "Alle Projekte compilieren" werden die Projekte in der Reihenfolge compiliert, in der sie in der Projektgruppe angeordnet sind. Denkt an Abhängigkeiten zwischen den Projekten, beim Compilieren eines Moduls müssen alle statisch eingebundenen Packages bereits fertig compiliert vorliegen. Damit diese gefunden werden können, muss die *.dcp des statisch eingebundenen Packages im Suchpfad des Moduls verfügbar sein.
Ich verwende folgende Einstellungen: Jedes Projekt hat sein eigenes lib-Verzeichnis, in dem compilierte Units (*.dcu) landen. Alle Projekte haben ein gemeinsames Ausgabeverzeichnis für *.dcp, welches auch in alles Projekten im Suchpfad steht. Auch das Ausgabeverzeichnis für die exe und die Packages (*.bpl) muss für alle Projekte gleich sein. Wenn das Ausgabeverzeichnis außerdem die verwendeten Runtime-Packages von Delphi und Drittanbietern enthält, dann enthält es genau die Dateien, die auch der Anwender benötigt.
Es ist vor einer Auslieferung an den Anwender unbedingt empfehlenswert, die Zusammenstellung von Dateien auf einem System ohne Delphi zu testen, damit man keine *.bpl vergisst.

Visual Form Inheritance (VFI) und Packages
Wenn man einem Programm (also einem exe-Projekt) eine bestehende Unit hinzufügt, zu der eine *.dfm-Datei existiert, dann erkennt Delphi das automatisch. Fügt man solch eine Unit einem Package hinzu, dann erkennt zumindest mein Delphi XE nicht, dass ein *.dfm existiert (es wird das reine Unit-Symbol für die Datei verwendet, nicht das Form-Symbol). Das hat zur Folge, dass eine visuell abgeleitete Form nicht geöffnet werden kann, weil Delphi die Basisform nicht findet, falls diese nicht zufällig gerade angezeigt wird (geöffnet ist). Zur Abhilfe muss man von Hand den Kommentar im contains-Abschnitt in der *.dpk-Datei einfügen (und ggf. das Projekt neu öffnen):
Delphi-Quellcode:
  MainView in 'MainView.pas',           // TMainView ohne *.dfm - Delphi macht Probleme!
  MainView in 'MainView.pas' {MainView}, // TMainView mit *.dfm - von Hand eingefügt
Namen der von Delphi mitgelieferten Packages
Wenn das Package vcl statisch eingebunden wird, dann gehört dazu die Datei vclxxx.bpl - xxx steht hier für die Delphi-Versionsnummer, beispielsweise 150 für Delphi XE. Selbsterstellte Packages werden natürlich ohne Delphi-Versionsnummer gesucht und gefunden.

Mitzuliefernde Dateien
  • Die exe
  • Alle statisch in die exe eingebundenen Laufzeitpackages (*.bpl), incl. der in diesen Packages statisch eingebunden Packages
  • Dynamisch eingebundene Packages, sofern diese verwendet werden können sollen, incl. der in diesen Packages statisch eingebunden Packages
Achtung: Auf dem Rechner, auf dem Delphi installiert ist, sind alle installierten Runtimepackage in einem Systemverzeichnis vorhanden, so dass die exe auch ohne (in delphi installierte) Packages lauffähig ist. Auf anderen Rechnern müssen die Packages (*.bpl) entweder im Programmverzeichnis oder in einem Systemverzeichnis (welches in der Systemvariable PATH enthalten ist) zur Verfügung gestellt werden.

Links
Ich freue mich über Feedback und werde diese HowTo ggf. später noch anpassen, so lange es die Editfunktion zulässt ;-)

Back2Code 11. Dez 2014 15:54

AW: HowTo: Runtime-Packages
 
Delphi-Quellcode:
        @InitModule := GetProcAddress(HPack, 'InitModule');
Ist das die einzige Möglichkeit auf Funktionen/Proceduren aus einem Package zugreifen zu können? :shock: Wie übergebe ich jetzt z.B Parameter von meiner Host Applikation an das Package?

himitsu 11. Dez 2014 17:22

AW: HowTo: Runtime-Packages
 
Indem man einen Prozedurtypen mit Parametern und eventuell Result deklariert?

Und das hier betrifft auch alles nur dynamisch geladene BPL.
Bei den statisch geladenen BPLs gibt es keinen Unterschied zum direkten Einbinden des Codes.

RSE 11. Dez 2014 17:26

AW: HowTo: Runtime-Packages
 
Wenn ein Package oder eine dll dynamisch zur Laufzeit geladen wurde, dann muss die Einsprungadresse für die Prozedur oder Funktion natürlich erst ermittelt werden. Das geht mit der Funktion GetProcAddress wie im Beispiel. Ob es noch weitere Möglichkeiten gibt, kann ich jetzt aus dem Kopf nicht sagen, aber eine reicht doch. Baue dir eine Initialisierungsfunktion, die dir alle Einsprungadressen ermittelt.

Wurde das Package statisch geladen, dann brauchst du diesen Weg natürlich nicht beschreiten.

Hier eine Version mit einer Funktion mit Parametern (ungetestet):
Delphi-Quellcode:
type
  TTestFunc: function(i: Integer): Integer; // muss mit dem Typ im Package übereinstimmen
var
  HPack: HModule;
  TestFunc: TTestFunc;
begin
  try
    HPack := LoadPackage('MyPackage.bpl');
    if HPack > 0 then
      try
        @TestFunc := GetProcAddress(HPack, 'TestFunc');
        if Assigned(TestFunc) then // Falls der Compiler hier meckert: @TestFunc verwenden - Die Compiler Magic bekommt auch nicht immer alles richtig hin...
          Showmessage(IntToStr(TestFunc(5)));
        else
          ShowMessage('Einsprungadresse für Funktion TestFunc nicht gefunden');
      finally
        UnloadPackage(HPack);
      end
    else
      ShowMessage('Das Laden des Packages "MyPackage.bpl" war nicht erfolgreich');
  except
    on E: EPackageError do
      ShowMessage('Fehler beim Laden des Packages "MyPackage.bpl":' +
        sLineBreak + E.Message);
  end;
end;


Alle Zeitangaben in WEZ +1. Es ist jetzt 22:59 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz