Ablaufsteuerung (Delphi)
Nach oben zu Delphi-Sprachreferenz - Index
Bevor Sie mit der Programmierung von Anwendungen beginnen, müssen Sie wissen, wie Parameter übergeben und Funktionsergebnisse verarbeitet werden. Die Übergabe ist von verschiedenen Faktoren abhängig. Dazu gehören die Aufrufkonventionen, die Parametersemantik sowie der Typ und die Größe des zu übergebenden Wertes.
Dieses Thema enthält Informationen zu folgenden Bereichen:
- Parameterübergabe
- Funktionsergebnisse
- Methodenaufrufe
- Exit-Prozeduren
Inhaltsverzeichnis
Parameterübergabe
Die Übergabe von Parametern an Prozeduren und Funktionen erfolgt entweder über CPU-Register oder über den Stack. Welche Übergabemethode verwendet wird, hängt von der Aufrufkonvention der Routine ab. Informationen über Aufrufkonventionen finden Sie im Thema "Aufrufkonventionen".
Übergabe per Wert oder per Referenz
Variablenparameter (var) werden immer per Referenz übergeben, also als 32-Bit-Zeiger auf die tatsächliche Speicherposition.
Wert- und Konstantenparameter (const) werden abhängig vom Typ und der Größe des Parameters als Wert oder als Referenz übergeben:
- Ein Parameter ordinalen Typs wird als 8-Bit-, 16-Bit-, 32-Bit- oder 64-Bit-Wert übergeben. Dabei wird dasselbe Format verwendet wie bei einer Variable des entsprechenden Typs.
- Ein Parameter reellen Typs wird immer im Stack übergeben. Ein Single-Parameter benötigt vier Byte, ein Double-, Comp- oder Currency-Parameter belegt acht Byte. Auch ein Real48-Parameter belegt acht Byte. Dabei wird der Real48-Wert in den niederwertigen sechs Byte gespeichert. Ein Extended-Parameter belegt zwölf Byte, wobei der Extended-Wert in den niederwertigen zehn Byte gespeichert wird.
- Ein kurzer String-Parameter wird als 32-Bit-Zeiger auf einen kurzen String übergeben.
- Lange String-Parameter und dynamische Array-Parameter werden als 32-Bit-Zeiger auf den dynamischen Speicherblock übergeben, der für den langen String reserviert wurde. Für einen leeren langen String wird der Wert nil übergeben.
- Ein Zeiger, eine Klasse, eine Klassenreferenz oder ein Prozedurzeiger wird als 32-Bit-Zeiger übergeben.
- Ein Methodenzeiger wird immer als zwei 32-Bit-Zeiger im Stack übergeben. Der Instanzzeiger wird vor dem Methodenzeiger auf dem Stack abgelegt, so dass der Methodenzeiger die niedrigere Adresse erhält.
- Mit den Konventionen register und pascal wird ein varianter Parameter als 32-Bit-Zeiger auf einen Variant-Wert übergeben.
- Mengen, Records und statische Arrays aus einem, zwei oder vier Byte werden als 8-Bit-, 16-Bit- und 32-Bit-Werte übergeben. Größere Mengentypen, Records und statische Arrays werden als 32-Bit-Zeiger auf den Wert übergeben. Eine Ausnahme von dieser Regel ist, dass bei den Konventionen cdecl, stdcall und safecall die Records immer direkt im Stack übergeben werden. Die Größe eines auf diese Weise übergebenen Records wird immer bis zur nächsten Double-Word-Grenze erweitert.
- Ein offener Array-Parameter wird in Form zweier 32-Bit-Werte übergeben. Der erste Wert ist ein Zeiger auf die Array-Daten. Der zweite Wert enthält die Anzahl der Array-Elemente minus eins.
Bei der Übergabe zweier Parameter im Stack belegt jeder Parameter immer ein Vielfaches von vier Byte (also eine ganzzahlige Anzahl von Double Words). Ein 8-Bit- oder 16-Bit-Parameter wird auch dann als Double Word übergeben, wenn er nur ein Byte oder ein Word belegt. Der Inhalt der nicht verwendeten Byte des Double Word ist nicht definiert.
Die Konventionen pascal, cdecl, stdcall und safecall
Bei Verwendung der Konventionen pascal, cdecl, stdcall und safecall werden alle Parameter im Stack übergeben. Bei der Konvention pascal werden die Parameter in der Reihenfolge ihrer Deklaration (von links nach rechts) übergeben, so dass der erste Parameter im Stack an der obersten Adresse und der letzte Parameter an der untersten Adresse gespeichert wird. Bei den Konventionen cdecl, stdcall und safecall werden die Parameter in der entgegengesetzten Reihenfolge ihrer Deklaration (von rechts nach links) übergeben, so dass der erste Parameter im Stack an der untersten Adresse und der letzte an der obersten Adresse gespeichert wird.
Die Konvention register
Bei der Konvention register werden maximal drei Parameter in den CPU-Registern übergeben, der Rest im Stack. Die Parameter werden in der Reihenfolge ihrer Deklaration übergeben (wie bei der Konvention pascal). Die ersten drei geeigneten Parameter stehen in den Registern EAX, EDX und ECX (in dieser Reihenfolge). Nur reelle, variante und strukturierte Typen sowie Methodenzeiger- und Int64-Typen sind als Registerparameter ungeeignet. Sind mehr als drei mögliche Registerparameter vorhanden, werden die ersten drei in EAX, EDX und ECX übergeben. Die restlichen Parameter werden in der Reihenfolge ihrer Deklaration im Stack abgelegt. Betrachten Sie beispielsweise die folgende Deklaration:
procedure Test(A: Integer; var B: Char; C: Double; const D: string; E: Pointer);
Hier übergibt die Prozedur Test den Parameter A in EAX als 32-Bit-Integer, B in EDX als Zeichenzeiger und D in ECX als Zeiger auf einen Speicherblock für einen langen String. C wird im Stack in Form zweier Double Words und E als 32-Bit-Zeiger (in dieser Reihenfolge) abgelegt.
Konventionen zur Speicherung in Registern
Prozeduren und Funktionen dürfen die Register EBX, ESI, EDI und EBP nicht verändern. Die Register EAX, EDX und ECX stehen jedoch zur Verfügung. Wenn ein Konstruktor oder Destruktor in Assembler implementiert wird, muss das DL-Register unverändert bleiben. Prozeduren und Funktionen werden unter der Voraussetzung aufgerufen, dass das Richtungsflag der CPU nicht gesetzt ist (entsprechend einer CLD-Anweisung). Auch nach Beendigung der Routine darf das Richtungsflag nicht gesetzt sein.
Hinweis: Delphi-Prozeduren und -Funktionen werden in der Regel unter der Voraussetzung aufgerufen, dass der CPU-Stack leer ist. Der Compiler versucht bei der Generierung von Code alle acht FPU-Stackeinträge zu verwenden.
Achten Sie bei der Arbeit mit MMX- und XMM-Anweisungen darauf, dass die Werte des xmm- und mm-Registers erhalten bleiben. Delphi-Funktionen werden unter der Voraussetzung aufgerufen, dass die x87 FPU-Datenregister für die x87-Gleitkommaanweisungen zur Verfügung stehen. Der Compiler setzt also voraus, dass die EMMS/FEMMS-Anweisungen nach den MMX-Operationen aufgerufen wurden. Delphi-Funktionen stellen keine Anforderungen an den Status und Inhalt von xmm-Registern. Sie gewährleisten auch nicht, dass der Inhalt der xmm-Register unverändert bleibt.
Funktionsergebnisse
Für die Rückgabe von Funktionsergebnissen gelten folgende Konventionen.
- Funktionsergebnisse ordinalen Typs werden, wenn möglich, in ein CPU-Register zurückgegeben. Bytes werden in AL, Words in AX und Double Words in EAX zurückgegeben.
- Die Funktionsergebnisse der Real-Typen werden im Top-of-Stack-Register des Coprozessors für Gleitkommazahlen (ST(0)) zurückgegeben. Bei Funktionsergebnissen vom Typ Currency wird der Wert von ST(0) um den Faktor 10000 skaliert. Beispielsweise wird der Currency-Wert 1,234 in ST(0) als 12340 zurückgegeben.
- Strings, dynamische Arrays, Methodenzeiger oder Varianten werden so zurückgegeben, als ob das Funktionsergebnis als zusätzlicher var-Parameter nach den übrigen Parametern deklariert worden wäre. Die aufrufende Routine übergibt also einen zusätzlichen 32-Bit-Zeiger auf eine Variable, über die das Funktionsergebnis zurückgeliefert wird.
- Int64 wird in EDX:EAX zurückgegeben.
- Zeiger, Klassen, Klassenreferenzen und Prozedurzeiger werden in EAX zurückgegeben.
- Statische Arrays, Records und Mengen werden in AL zurückgegeben, wenn der Wert ein Byte belegt, in AX, falls der Wert zwei Byte belegt, und in EAX, falls vier Byte benötigt werden. Andernfalls wird der Funktion nach den deklarierten Parametern ein zusätzlicher var-Parameter übergeben, über den die Funktion das Ergebnis zurückliefert.
Methodenaufrufe
Für Methoden werden dieselben Aufrufkonventionen wie für normale Prozeduren und Funktionen verwendet. Jede Methode verfügt jedoch über den zusätzlichen Parameter Self. Dabei handelt es sich um eine Referenz auf die Instanz oder Klasse, in der die Methode aufgerufen wird. Der Parameter Self wird als 32-Bit-Zeiger übergeben.
- Bei der Konvention register verhält sich der Parameter Self, als ob er vor allen anderen Parametern deklariert worden wäre. Er wird somit immer im Register EAX übergeben.
- Bei der Konvention pascal verhält sich der Parameter Self, als ob er nach allen anderen Parametern (einschließlich des zusätzlichen var-Parameters für das Funktionsergebnis) deklariert worden wäre. Er wird somit als letzter Parameter übergeben und hat von allen Parametern die niedrigste Adresse.
- Bei den Konventionen cdecl, stdcall und safecall verhält sich der Parameter Self, als ob er vor allen anderen Parametern, aber nach dem zusätzlichen var-Parameter für das Funktionsergebnis deklariert worden wäre. Er wird daher als letzter Parameter, aber vor dem zusätzlichen var-Parameter (falls vorhanden) übergeben.
Konstruktoren und Destruktoren verwenden dieselben Aufrufkonventionen wie die anderen Methoden. Es wird jedoch ein zusätzlicher Boolescher Flag-Parameter übergeben, der den Kontext des Konstruktor- oder Destruktoraufrufs anzeigt.
Der Wert False im Flag-Parameter eines Konstruktoraufrufs zeigt an, dass der Konstruktor über ein Instanzobjekt oder mit dem Schlüsselwort inherited aufgerufen wurde. In diesem Fall verhält sich der Konstruktor wie eine normale Methode. Der Wert True im Flag-Parameter eines Konstruktoraufrufs zeigt an, dass der Konstruktor über eine Klassenreferenz aufgerufen wurde. In diesem Fall erzeugt der Konstruktor eine Instanz der mit Self referenzierten Klasse und gibt in EAX eine Referenz auf das neu erzeugte Objekt zurück.
Der Wert False im Flag-Parameter eines Destruktoraufrufs zeigt an, dass der Destruktor mit dem Schlüsselwort inherited aufgerufen wurde. In diesem Fall verhält sich der Destruktor wie eine normale Methode. Der Wert True im Flag-Parameter eines Destruktoraufrufs zeigt an, dass der Destruktor über ein Instanzobjekt aufgerufen wurde. In diesem Fall gibt der Destruktor die mit Self bezeichnete Instanz frei, bevor er beendet wird.
Der Flag-Parameter verhält sich so, als ob er vor allen anderen Parametern deklariert worden wäre. Bei der Konvention register wird er im Register DL übergeben. Bei pascal erfolgt die Übergabe vor allen anderen Parametern. Bei den Konventionen cdecl, stdcall und safecall wird er direkt vor dem Parameter Self übergeben.
Der Wert von DL muss deshalb vor der Beendigung des Programms wiederhergestellt werden, damit BeforeDestruction oder AfterConstruction ordnungsgemäß aufgerufen werden kann.
Exit-Prozeduren
Mit Exit-Prozeduren können Sie sicherstellen, dass vor Beendigung eines Programms bestimmte Aktionen (z. B. das Aktualisieren und Schließen von Dateien) eingeleitet werden. Mithilfe der Zeigervariablen ExitProc kann eine Exit-Prozedur installiert werden, die bei jeder Beendigung des Programms aufgerufen wird. Dabei ist es gleichgültig, ob das Programm normal, über einen Aufruf von Halt oder aufgrund eines Laufzeitfehlers beendet wird. Einer Exit-Prozedur werden keine Parameter übergeben.
Hinweis: Es empfiehlt sich, alle Abläufe bei der Programmbeendigung nicht über Exit-Prozeduren, sondern über finalization-Abschnitte zu steuern. Exit-Prozeduren können nur für ausführbare Dateien verwendet werden. Für DLLs (Win32) können Sie eine ähnliche Variable verwenden: Sie heißt DllProc und wird sowohl beim Laden als auch beim Entladen der Bibliothek aufgerufen. Bei der Verwendung von Packages muss das gewünschte Verhalten in einem finalization-Abschnitt implementiert werden. Alle Exit-Prozeduren werden aufgerufen, bevor die finalization-Abschnitte ausgeführt werden.
Exit-Prozeduren können von Units und von Programmen installiert werden. Eine Unit kann eine Exit-Prozedur im initialization-Abschnitt installieren. Die Prozedur ist dann für die erforderlichen Aufräumarbeiten verantwortlich (z. B. das Schließen von Dateien).
Bei korrekter Implementierung ist jede Exit-Prozedur ein Glied in einer Kette von Exit-Prozeduren. Alle Prozeduren in der Kette werden in der umgekehrten Reihenfolge ihrer Installation ausgeführt. Dadurch ist sichergestellt, dass der Beendigungscode einer bestimmten Unit nicht vor dem Beendigungscode der Units ausgeführt wird, die von ihr abhängen. Um die Beendigungskette nicht zu unterbrechen, müssen Sie den aktuellen Inhalt von ExitProc speichern, bevor Sie ihr die Adresse Ihrer eigenen Beendigungsprozedur zuweisen. Außerdem muss der gespeicherte Wert von ExitProc in der ersten Anweisung Ihrer Beendigungsprozedur wiederhergestellt werden.
Das folgende Beispiel skizziert die Implementierung einer solchen Prozedur:
var
ExitSave: Pointer;
procedure MyExit;
begin
ExitProc := ExitSave; // Zuerst immer den alten Vektor wiederherstellen .
.
.
.
end;
begin
ExitSave := ExitProc;
ExitProc := @MyExit;
.
.
.
end.
Zuerst wird der Inhalt von ExitProc in ExitSave gespeichert. Anschließend wird die Prozedur MyExit installiert. Nachdem die Prozedur als Teil des Beendigungsvorgangs aufgerufen wurde, wird mit MyExit zuerst die bisherige Exit-Prozedur wiederhergestellt.
Die Beendigungsroutine der Laufzeitbibliothek ruft Exit-Prozeduren auf, bis ExitProc den Wert nil annimmt. Um Endlosschleifen zu vermeiden, wird ExitProc vor jedem Aufruf auf nil gesetzt. Die nächste Exit-Prozedur wird somit nur aufgerufen, wenn ExitProc in der aktuellen Exit-Prozedur eine Adresse zugewiesen wird. Tritt in einer Exit-Prozedur ein Fehler auf, wird sie nicht erneut aufgerufen.
Eine Exit-Prozedur kann die Ursache einer Beendigung feststellen, indem sie die Integer-Variable ExitCode und die Zeigervariable ErrorAddr auswertet. Bei einer normalen Beendigung hat ExitCode den Wert Null und ErrorAddr den Wert nil. Wird ein Programm durch einen Aufruf von Halt beendet, enthält ExitCode den der Funktion Halt übergebenen Wert und ErrorAddr den Wert nil. Wird das Programm aufgrund eines Laufzeitfehlers beendet, enthält ExitCode den Fehlercode und ErrorAddr die Adresse der ungültigen Anweisung.
Die letzte Exit-Prozedur (die von der Laufzeitbibliothek installiert wird) schließt die Ein- und Ausgabedateien. Hat ErrorAddr nicht den Wert nil, wird eine Meldung über den Laufzeitfehler ausgegeben. Wenn Sie selbst Meldungen zu Laufzeitfehlern ausgeben wollen, installieren Sie eine Exit-Prozedur, die ErrorAddr auswertet und eine Meldung ausgibt, wenn die Variable nicht den Wert nil hat. Zusätzlich müssen Sie vor dem Ende der Prozedur den Wert von ErrorAddr auf nil setzen, so dass der Fehler nicht in anderen Exit-Prozeduren erneut ausgegeben wird.
Nachdem die Laufzeitbibliothek alle Exit-Prozeduren aufgerufen hat, wird die Steuerung an das Betriebssystem zurückgegeben und der in ExitCode gespeicherte Wert als Rückgabewert übergeben.