Unter der Lupe: Delphi packages

By: Willibald Krenn

Abstract: Kleiner Exkurs in das Delphi/Kylix "ABI" und Verweis auf "schmutzige Tricks", die Packages ermvglichen. Von Willibald Krenn.

Es ist schon eine Zeit lang her, daß Borland mit Delphi 3 zum ersten mal Packages der Delphi-Entwicklergemeinde vorgestellt hat. Damals war die Entwicklergemeinde bezüglich der Neuerung geteilter Meinung: So begrüßten die einen die neuen Möglichkeiten, die Ihnen Packages in die Hand gaben, während die anderen den Tag verfluchten, an dem wohl auch bei unserem Delphi die DLL Hölle ausgebrochen zu sein schien.

Auch heute noch kann ich mich manchmal des Eindrucks nicht erwehren, daß Packages oft ein viel zu wenig verstandenes - und daher auch ungenutztes - Feature von Delphi sind. Viele Delphi Entwickler möchten sich scheinbar von der "ein EXE" Philosophie nicht trennen und lassen Packages deshalb oft links liegen, was dann manchmal zu den kuriosen "wie kann ich in DLLs Objekte benutzten" - Fragen führt, wenn sie ihr Projekt doch modularisieren.

Um es gleich vorweg zu nehmen, auch ich habe es einmal mit dem "harten Weg" versucht: Für ein Programm hatte ich mir damals einen Weg ausgedacht, wie ich MDI-Child Formulare in DLL Dateien auslagern konnte um diese dann bei Bedarf zu laden und zu erzeugen. Möglich gemacht haben das ein paar Veränderungen am VCL Source-Code, die Weitergabe der wichtigsten globalen Objektvariablen sowie eine gehörige Portion Spieltrieb um das System überhaupt einmal zum Laufen zu bringen.

Schlußendlich hat es dann auch tatsächlich funktioniert und auch einen Port nach Delphi 5 überstanden, aber spätestens da ist mir dann klar geworden daß diese Vorgehensweise einfach keinen Sinn macht, denn die Klimmzüge in den VCL Sourcen sind immer mehr geworden.

Über den Umweg der Pascal Skripting Tools (später mehr dazu), bin ich dann bei den Packages gelandet und seitdem von ihnen und den Möglichkeiten, die sie bieten ganz fasziniert!

Einige Vorteile von Packages, die ich sehe, sind:

  • bessere Debugging-Unterstützung als bei normalen DLLs
  • Nutzung aller Object-Pascal Sprachkonstrukte - ohne Hintertürchen
  • zusätzlich vorhandene Möglichkeit Symbole wie in DLLs per Export zu veröffentlichen

Mit Delphi 6 hat Borland zudem die RTL aus dem VCL Verbund herausgenommen und in ein eigenes Package gesteckt.

Kommen wir also nun zum Standard Einsatzgebiet von Delphi-Packages: Im Prinzip finden wir sie überall dort, wo Modularisierung von Delphi Applikationen gefragt ist und da vor allem dann, wenn sie plattfromübergreifend funktionieren soll. Zu diesem Thema gibt es im Internet auch schon seit langem Design Patterns: Siehe z.B. https://www.inquiry.com/techtips/delphi_pro/10min/10min0301-1.asp von Bob Swart.

In diesem Artikel möchte ich einen (im Gegensatz zu Bob Swart) etwas extremeren Weg gehen und ein paar "schmutzige Tricks" vorstellen, die man mit Hilfe von Packages "hochleben" lassen kann. Damit diese Frage erst gar nicht aufkommt: "Schmutzig" sind die Tricks deswegen, weil sie auf - meines Wissens nach - undokumentierten Features von Borland Delphi beruhen.

Undokumentiert

Um überhaupt irgend etwas mit einem Package außerhalb von Delphi anfangen zu können, muß man einmal den Aufbau desselben kennen. Zugegeben - das ist der einfache Part: Packages sind "magische" DLLs - so werden sie zumindest in manchen Quellen beschrieben.

Packages sind natürlich normale DLLs bzw. ELF - SOs, aber etwas besonderes haben sie schon an sich: Delphi exportiert automatisch alle Symbole, die zusätzlich auch noch "gemangled" (="zerstückelt") werden. Das ganze sieht dann (ab Delphi 4) zum Beispiel so aus:

@$xp$21Classes@THandleStream

Um dahinter zu kommen, was das eigentlich genau bezeichnet, ist es sehr hilfreich, wenn man sich Borlands TDump Utility bedient. TDump kann nämlich die Symbole C++ "de-mangln" - sprich in mehr oder weniger leserliches C++ übersetzten.

Vergleicht man dieses Ergebnis danach mit der Urform in der BPL Datei, so lassen sich folgende Gesetzmäßigkeiten ermitteln (alle Angaben ohne Gewähr):

Delphi - Datentyp
mangled
Anmerkung
real48 Real48 ²
real d (= double)
single f
double d
extended g
Currency System@Currency ²
Comp System@Comp ²
shortint zc see byte
smallint s
integer i (= longint)
byte uc see shortint
longint i
int64 j
word us
boolean o
bytebool uc
wordbool us
longbool i
char c
widechar b
longword ui (= cardinal)
PChar c ³
shortstring System@%SmallString$iuc$255% ²
string System@AnsiString ²
ansistring System@AnsiString ²
variant System@Variant ²
olevariant System@OleVariant ²
widestring System@WideString ²
- v "void"
object <Unit>@<ClassName> ²

²) Längenangabe vor dem String

³) Pointer type!!

Sieht ja soweit gar nicht allzu kompliziert aus. Nur der ShortString Datentyp ist etwas "komisch" kodiert könnte man meinen. Wie auch immer, zusätzlich zu den "Namen" der Datentypen gibt es auch noch Steuercodes, die im zerstückelten Namen vorkommen können:

Zweck Zeichen Anmerkung
Konstante x
Pointer p
Übergabe per Referenz r
Unsigned u
Pointer auf Array a
FastCall qqr "register"
StdCall qqs
Cdecl q
Pascal Q ²
TypeInfo xp
Repeat t
"Break" $
"Bracket" %
"Dot" @
Constructor ctr
Destructor dtr

²) Symbolname in Großbuchstaben!

Hat man das soweit einmal beisammen, dann fehlen einem nur mehr die Regeln, nach denen die Zerstückelung des Funktionsnamen vor sich geht.

Alle "mangled" Symbole beginnen einmal mit einem "@" und haben dann folgenden Aufbau:

Typ Aufbau Anmerkungen
RTTI @$xp$<UnitName>@<SymbolName> TypeInfo(<SymbolName>)
Klassen @<UnitName>@<KlassenName>@
Variablen @<UnitName>@<VarName>
Funktion @<UnitName>@<FuncName>$<CallingConvention><Args>
Methode @<UnitName>@<ClassName>@<MethodName>$<CallingConvention><Args>
Constr @<UnitName>@<ClassName>@$bctr$<CallingConvention><Args> ²
Destr @<UnitName>@<ClassName>@$bdtr$<CallingConvention><Args> call .Free..

²) TConstructor = function(AClass: TClass; Call_New_Instance: Integer = 1; <Args>): TObject;

Achtung: <UnitName> ist der Name der Unit kleingeschrieben - bis auf das erste Zeichen, welches groß geschrieben wird.

Es gibt dann noch einige Sonderformen (@<UnitName>@@<FuncName>$<Args>), die den Compiler-Magic Funktionen von Delphi entsprechen, aber für uns ohne Bedeutung sind.

Der eindeutig schwierigere Teil ist es herauszufinden, wie Delphi die Funktionsargumente übersetzt. Da mir - wie schon bereits erwähnt - keine offizielle Information von Borland darüber bekannt ist, fußt natürlich auch folgende Darstellung auf meinen eigenen Erkenntnissen/Vermutungen, die höchst wahrscheinlich in dem einen oder anderen Aspekt fehlerhaft sein werden.

Nun gut, wie werden also die Argumente von Funktionen bzw. Methoden kodiert? Einfach wäre es natürlich, wenn die Argumente einfach 1:1 per obiger Liste über- und eingesetzt das gewünschte Resultat brächten - allein, dem ist leider nicht so. Dem aufmerksamen Leser wird sicher schon die - von mir so bezeichnete - "Repeat" Anweisung in der 2 Tabelle von oben aufgefallen sein. Diese Anweisung wird immer dann eingesetzt, wenn sich zwei Argumente gleichen. Doch zuerst möchte ich noch kurz auf den allgemeinen Aufbau eines kodierten Parameters simplen Typs (keine Arrays!) eingehen, bevor ich auf die Spezialitäten zu sprechen kommen werde.

Den Aufbau könnte man wie folgt beschreiben:

[r][p][x]<encoded ArgString>
"r" (optional, da in "[]") steht dabei wohl für "Referenz" und ist z.B. bei allen "var" Argumenten gesetzt
"p" steht für "Pointer"
"x" steht für konstant und ist bei allen als "const" gekennzeichneten Argumenten gesetzt

Ein Beispiel sollten das Vorgehen verdeutlichen:

"rpc" steht z.B. für "var x:PChar"

Doch wofür steht "@Testunit@Test1$qqrrixdt1"? Nach unserem bisherigen Wissen, können wir folgendes ermitteln:

kodierter String Bedeutung in Delphi
qqr fastcall
ri var integer
xd const double
t1 -->?

Hier taucht nun zum ersten Mal das "Repeat" Zeichen auf und es bringt gleich ein Argument mit - nämlich die Eins. Der Gedanke ist nun ziemlich naheliegend, daß man anstatt des Repeat Zeichens einfach den ersten Parameter noch einmal einzusetzen hat, daß also aus "t1" ein "ri" werden sollte. (Aus "t2" würde dann "xd" etc.) Wie sollte es auch anderes sein - das ist schon die Lösung des Beispiels. In Delphi wurde die Funktion folgendermaßen deklariert:


Procedure Test1(var a: Integer; const c:Double; var b:Integer);

Das Unerfreuliche an dieser "Repeat" Instruktion ist nun, daß ich noch nicht ermitteln konnte, wann genau sie eingesetzt wird! Denn eine Funktion


Procedure Test1(const a: Double; const c:Double; var b:Integer);

sollte nach bisherigem Wissen ja in @Test1$qqrxdt1ri kodiert werden, was aber nicht der Fall ist. Es scheint überhaupt so zu sein, daß als konstant bezeichnete Argumente und Argumente ohne einen Bezeichner (const/var/out) nicht durch ein Repeat Zeichen ersetzt werden, wenn sie kurz sind. Mit "kurz" meine ich hier Argumente, deren kodierter Name nicht über eine Längenangabe am Anfang verfügt. Argumente, die per Referenz übergeben werden, scheinen hingegen immer durch ein Repeat Zeichen ersetzt zu werden, ganz gleich wie lange der kodierte Argumentenname nun tatsächlich ist.

Als kleiner Nachtrag: Die obige Prozedur wird als ...@Test1$qqrxdxdri kodiert gespeichert.

Verlassen wir nun die Gefielde der einfachen Argumente und schwingen wir uns auf in neue Sphären! Wie werden z.B. Arrays kodiert?


type
  MyArray = array[1..10] of Integer;
  MyChar = 'A'..'Z';

Procedure Test1a(const i:Array of String);
Procedure Test1b(i:Array of Const);
Procedure Test2(const i:MyArray);
Procedure Test3(i:MyChar);

Delphi macht diese Funktionen unter folgenden Namen zugänglich:

@Test1a$qqrpx17System@AnsiStringxi

@Test1b$qqrpx14System@TVarRecxi

@Test2$qqrpxi

@Test3$qqrc

Eigentlich sollte diese Kodierung keinerlei große Überraschungen beinhalten, aber gestatten Sie mir trotzdem den Hinweis auf den versteckten konstanten Integer Parameter, der bei offenen Arrays mit übergeben wird. (Wahrscheinlich handelt es sich dabei um die Array Länge.) Außerdem werden die Arrays als Pointer übergeben.

Etwas komplizierter wird das Ganze, sollten wir einer Funktion einen Pointer auf ein Array übergeben! Also zum Beispiel eine solche Konstruktion:


type
  PMyArray = ^MyArray;
  MyArray = array[1..10] of Integer;

Procedure Test(const i:PMyArray);

Die Testfunktion wird so kodiert: @Testunit@Test$qqrpxa10$i. Als Bildungsvorschrift könnte man also folgende Zeichenkette angeben:

p[x]a<Länge>$<Array_Typ>
<Länge> bezeichnet die Anzahl der Elemente im Array
<Array_Typ> wird ganz normal nach der Argumenten - Bildungsvorschrift generiert! Sollte es sich also um ein Array von Pointern auf z.B. TPropInfo handeln, dann wird "p17Typinfo@TPropInfo" als Array Typ eingesetzt!

Bis jetzt sind wir ja noch nicht auf allzu große Probleme gestoßen - aber das kann sich schnell ändern, wenn wir uns Mengen genauer ansehen! Denn was meinen Sie, wie könnte folgende Delphi Funktion unkodiert aussehen: @Testunit@Test$qqr29System@%Set$tc$iuc$65$iuc$90% ? Des Rätsels Lösung:


type
  MyChar = 'A'..'Z';
  MySet = Set of MyChar;

Procedure Test(i:MySet);

An dieser Stelle kann man wieder einmal nur vermuten, was die einzelnen Zeichen bedeuten.

So könnte "$t" z.B. für "Type" stehen und "$i" vielleicht für "Index". Der verbleibende Rest hingegen sollte ziemlich eindeutig sein:

<Länge des kodierten Strings>System@%Set$t<Set_Type>$iuc$<Set_Low>$iuc$<Set_High>%
<Set_Type> bezeichnet den Typ der Menge und wird ganz normal kodiert.
Da eine Menge nur über 256 Elemente verfügen kann, bleibt der Indextyp immer unsigned char (also uc)

Mit diesem Wissen gerüstet, kann man auch leichter nachvollziehen, warum ein Shortstring als "System@%SmallString$iuc$255%" gespeichert wird. Wenn man zusätzlich noch bedenkt, daß wir in Delphi auch eigene kurze String - Typen deklarieren können, dann wird einem endgültig bewußt, daß die Länge des Strings in die Kodierung des Arguments mit einbezogen werden muß, soll das ganze typsicher sein. So nebenbei sei mir noch gestattet darauf hinzuweisen, daß Strings immer per Referenz übergeben werden. Sollten Sie das String Argument als konstant markiert haben, dann bekommt der String ein "rx" Präfix, ansonsten gibt sich Delphi mit einem einfachen "r" als Präfix zufrieden.

Als kleines Zwischenergebnis hier nun die Kodierungsvorschrift für kurze String Typen:

<Länge des kodierten Strings>System@%SmallString$iuc$<High>%
<High> bezeichnet die Anzahl der Elemente im ShortString - maximal also 255
Da ein Shortstring max. nur über 255 Zeichen verfügen kann, bleibt der Indextyp immer unsigned char (also uc)
Der Lower-Bound des Shortstrings braucht nicht mit abgespeichert zu werden, da er per Definition immer 0 ist.

Als Abschluß unseres kleinen Exkurses in das Borland Delphi, Kylix Application Binary Interface möchte ich Ihnen noch die Kodierungsvorschriften für Prozedur-, Methodenzeiger und Interfaces vorstellen.

Prozedurzeiger: p<CallingConvention><Args>$<ResultArg>
Methodenzeiger: ynp<CallingConvention><Args>$<ResultArg>
<Args> normal kodierte Argumente des Prozedur/Methodenzeigers
<ResultArg> normal kodierter Returntype des Prozedur/Methodenzeigers (Bei Prozeduren void)

Falls Sie sich hier angelangt fragen, wie Delphi mit Records als Funktionsargumente umgeht, dann kann ich Ihnen mitteilen, daß Delphi Records wie Objekttypen behandelt. (Es wird ganz einfach der Name verwendet.) Interfaces hingegen werden so kodiert:

<Länge des kodierten Strings>System@%DelphiInterface$t<Typ>%
<Typ> bezeichnet den kodierten Namen des Interfaces (wie immer ein vollständig qualifizierter Name!)

Hierbei möchte ich es nun belassen und Ihnen den Nutzen des Ganzen vorstellen!

Anwendungsmöglichkeiten

Leider leben wir heute in einer Zeit, in der sich jede Investition sofort rentieren muß und daher ist wohl (oder gerade) auch bei diesem Artikel die Frage nach dem "Wozu" sicher nicht ausgeblieben.

Nun gut, also was nützt es einem Delphi Entwickler undokumentierte Features seiner Programmiersprache zu kennen? Ich würde sagen, es fördert ein tieferes Verständnis für die Abläufe hinter den Kulissen: Aus dem bisher Erschlossenen ergibt sich ja auch zwangsläufig die Einschränkung der Packages, daß jede Unit nur einmal in allen geladenen Packages vorkommen darf.

Weiters können wir RuntimeTypeInformationen von beliebigen Klassen (Typen) abfragen, eine TObject Meta-Klasse mit beliebigen Klassen füllen, auf exportierte Variablen zugreifen und auch alle Klassen die von TPersistent abgeleitet sind ohne deren Namen zu kennen registrieren!

Nun gut, gerade viel ist das ja nicht - aber hier stehenzubleiben hieße auf halbem Weg umkehren! Ich weiß schon, daß man dieses Wissen als normaler Delphi Entwickler nicht benötigt, aber es gibt zumindest einen Typ von Anwendungen, der ganz "heiß" auf solche Infos ist: Scripting Engines für Delphi.

Denn wenn man jetzt noch Methoden zur Hand hätte, mit deren Hilfe man die Funktionen in der DLL aufrufen könnte, dann wäre es möglich jede beliebige Klasse in einem Package zu instantiieren und beliebige Funktionen/Methoden auszuführen. Das wiederum würde bedeuten, daß man für Pascal Source keinen Wrapper-Code mehr schreiben müßte um innerhalb des Interpreters auf die ungleich schnelleren, kompilierten Funktionen zurückgreifen zu können. Die einzige Voraussetzung wäre die Anwendung mit RuntimePackages zu erstellen.

Im Rahmen des DelphiWebScript Open Source Projekts wird unter anderem auch an Klassen gearbeitet, die diese Funktion bieten. Nebenbei bemerkt ist es ein einfaches diese Klassen derart zu erweitern, daß man damit auch ganz normale Funktionen in DLLs aufrufen kann. Zusätzlich sind diese Klassen ziemlich abgekapselt vom Interpreter und lassen sich daher auch ohne gröbere Schwierigkeiten in normale Anwendungen einbauen. Es sei allerdings darauf verwiesen, daß die Klassen unter der MPL - Lizenz stehen und Änderungen am Code somit veröffentlicht werden müssen. Sie können allerdings ohne Probleme auch in kommerziellen Anwendungen eingesetzt werden.

Es würde den Rahmen diese Artikels aber endgültig sprengen, wenn ich an dieser Stelle genauer auf die Funktionsweise der Klassen eingehen würde - daher nur ein paar Stichworte dazu:

supported not supported
fastcall stdcall, cdecl, pascal
(Strukturen aber vorbereitet)
Klassen Interfaces
simple types Arrays, Sets

Um eine Funktion in einem Package aufzurufen sind folgende Schritte nötig:

var
  FFunc: TPackageFunction;
  Arg: TBaseType;

....
FFunc := TPackageFunction.Create;
FFunc.FunctionName := 'Test';
FFunc.UnitName := 'Testunit';
FFunc.PackageName := 'MyPackage.bpl';
// z.B.
FFunc.FunctionType := func
// z.B.
Arg := TStringType.Create;
Arg.Variable := True;
Arg.Value := 'Hello';
FFunc.AddFunctionParam(Arg);
FFunc.ResultType := TIntegerType.Create;

FFunc.Call;

....

Natürlich beinhaltet die Unit auch Klassen um auf Variablen, Klassen und Methoden (etc.) zugreifen zu können und das ganze funktioniert natürlich auch unter Linux. Hier finden Sie ein paar Screenshots - quasi als "Beweis".

Sollten Sie mehr Informationen zu den angesprochenen Klassen erhalten wollen, so nehmen Sie bitte mit mir Kontakt auf.

Abschließend möchte ich Ihnen noch ganz herzlich für Ihr Interesse danken und hoffe, daß Sie von diesem Artikel etwas für sich und Ihre Arbeit mit Delphi mitnehmen konnten.

Willibald Krenn, 2001, Graz, Austria


Server Response from: ETNASC02