Contrôle des programmes (Delphi)
Remonter à Guide du langage Delphi - Index
Vous devez vous familiariser avec les concepts de transmission de paramètres et de traitement des résultats de fonction avant de passer aux projets de votre application. Le traitement des paramètres et du résultat des fonctions est déterminé par plusieurs facteurs dont les conventions d'appel, la sémantique des paramètres et le type ou la taille de la valeur transmise.
Cette section aborde les sujets suivants :
- Transfert de paramètres.
- Gestion des résultats de fonction.
- Gestion des appels de méthodes.
- Présentation des procédures de sortie.
Sommaire
Transfert de paramètres
Les paramètres sont transmis aux procédures et aux fonctions par l'intermédiaire des registres CPU ou de la pile, selon la convention d'appel de la routine. Pour plus d'informations sur les conventions d'appel, voir la rubrique Conventions d'appel.
Par valeur et par référence
Les paramètres variable (var) sont toujours transmis par référence. Il s'agit de pointeurs 32 bits indiquant l'emplacement de stockage réel.
Les paramètres valeur et constante (const) sont transmis par valeur ou par référence, selon le type et la taille du paramètre :
- Un paramètre scalaire est transmis sous la forme de valeur 8 bits, 16 bits, 32 bits ou 64 bits, en utilisant le même format qu'une variable de type correspondant.
- Un paramètre réel est toujours transmis sur la pile. Un paramètre Single occupe 4 octets, et un paramètre Double, Comp ou Currency occupe 8 octets. Un paramètre Real48 occupe 8 octets, avec la valeur Real48 stockée dans les 6 octets inférieurs. Un paramètre Extended occupe 12 octets, avec la valeur Extended stockée dans les 10 octets inférieurs.
- Un paramètre chaîne courte est transmis sous la forme d'un pointeur 32 bits sur une chaîne courte.
- Un paramètre de chaîne longue ou tableau dynamique est transmis sous la forme d'un pointeur 32 bits sur le bloc mémoire dynamique alloué pour la chaîne longue. La valeur nil est transmise pour une chaîne longue vide.
- Un paramètre pointeur, classe, référence de classe ou pointeur de procédure global est transmis sous la forme d'un pointeur 32 bits
- Un pointeur de méthode est transmis sur la pile sous la forme de pointeurs 32 bits. Le pointeur d'instance est empilé avant le pointeur de méthode afin que ce dernier occupe la plus petite adresse.
- Avec les conventions register et pascal, un paramètre variant est transmis sous la forme d'un pointeur 32 bits à une valeur Variant.
- Les tableaux, les enregistrements et les ensembles de 1, 2 ou 4 octets sont transmis sous la forme de valeurs 8 bits, 16 bits et 32 bits. Les tableaux, enregistrements et ensembles plus grands sont transmis sous la forme de pointeurs 32 bits sur la valeur. Exception à cette règle : les enregistrements sont toujours transmis directement sur la pile avec les conventions cdecl, stdcall et safecall. La taille d'un enregistrement transmis de cette manière est toujours arrondie à la limite supérieure la plus proche de deux mots.
- Un paramètre tableau ouvert est transmis sous la forme de valeurs 32 bits. La première valeur 32 bits est un pointeur sur les données du tableau. La deuxième valeur 32 bits représente le nombre d'éléments du tableau moins un.
Quand deux paramètres sont transmis dans la pile, chacun d'entre eux occupe un multiple de 4 octets (un nombre entier de deux mots). Pour un paramètre 8 bits ou 16 bits, même si le paramètre n'occupe qu'un octet ou un mot, il est transmis sous la forme d'un double mot. Le contenu des parties inutilisées du double mot n'est pas défini.
Conventions Pascal, cdecl, stdcall et safecall
Avec les conventions pascal, cdecl, stdcall et safecall, tous les paramètres sont transmis sur la pile. Pour la convention pascal, les paramètres sont empilés dans leur ordre de déclaration (gauche vers droite), afin que le premier paramètre se termine à l'adresse la plus haute et que le dernier paramètre se termine à l'adresse la plus basse. Pour les conventions cdecl, stdcall et safecall, les paramètres sont empilés dans l'ordre inverse de leur déclaration (droite vers gauche), afin que le premier paramètre se termine à l'adresse la plus basse et que le dernier paramètre se termine à l'adresse la plus haute.
Convention register
Avec la convention register, jusqu'à trois paramètres sont transmis dans les registres CPU et le reste (s'il existe) est transmis sur la pile. Les paramètres sont transmis dans l'ordre de leur déclaration (comme avec la convention pascal). Les trois premiers paramètres retenus sont transmis respectivement dans les registres EAX, EDX et ECX. Les types réels, pointeur de méthode, variant, Int64 et structurés ne sont pas retenus comme paramètres registre, mais tous les autres types de paramètre le sont. Si plus de trois paramètres sont utilisables comme paramètres registre, les trois premiers sont transmis dans EAX, EDX et ECX, et les paramètres restants sont empilés sur la pile dans l'ordre de leur déclaration. Par exemple, soit ces déclarations :
procedure Test(A: Integer; var B: Char; C: Double; const D: string; E: Pointer);
un appel à Test transmet A dans EAX sous la forme d'un entier 32 bits, B dans EDX sous la forme d'un pointeur sur un Char, et D dans ECX sous la forme d'un pointeur sur un bloc mémoire de chaîne longue. De plus, C et E sont empilés, dans cet ordre, sous la forme, respectivement d'un double mot et d'un pointeur 32 bits.
Règles de sauvegarde des registres
Les procédures et les fonctions doivent préserver la valeur des registres EBX, ESI, EDI et EBP, mais, elles peuvent modifier les registres EAX, EDX et ECX. Quand vous implémentez un constructeur ou un destructeur en assembleur, assurez-vous de préserver le registre DL. Les procédures et fonctions du langage Delphi sont toujours appelées en supposant que l'indicateur de direction de la CPU est effacé (ce qui correspond à une instruction CLD) et, à la sortie des routines, cet indicateur doit également être effacé.
Remarque : Les procédures et les fonctions du langage Delphi sont généralement invoquées avec l'hypothèse que la pile FPU est vide. Le compilateur essaie d'utiliser les huit entrées de la pile FPU lorsqu'il génère le code.
Quand vous manipulez des instructions MMX et XMM, veillez à préserver les valeurs des registres xmm et mm. Les fonctions Delphi sont invoquées avec l'hypothèse que les registres de données FPU x87 peuvent être utilisés par des instructions en virgule flottante x87. C'est-à-dire que le compilateur suppose que l'instruction EMMS/FEMMS a été appelée après les opérations MMX. Les fonctions Delphi ne font aucune hypothèse sur l'état et le contenu des registres xmm. Elles ne garantissent pas que le contenu des registres xmm sera inchangé.
Gestion des résultats de fonction
Les conventions suivantes sont utilisées pour renvoyer des valeurs de résultat des fonctions :
- Les résultats scalaires sont renvoyés, quand c'est possible, dans un registre CPU : les octets sont renvoyés dans AL, les mots dans AX, les doubles mots dans EAX.
- Les résultats réels sont renvoyés dans le registre du haut de la pile du coprocesseur à virgule flottante (ST(0)). Pour les résultats de fonction de type Currency, la valeur dans ST(0) est graduée par 10 000. Par exemple, la valeur Currency 1.234 est renvoyée en ST(0) en 12340.
- Pour un résultat chaîne, tableau dynamique, pointeur de méthode ou variant, les effets sont les mêmes que si le résultat de la fonction était déclaré en tant que paramètre var supplémentaire suivant les paramètres déclarés. En d'autres termes, l'appelant passe un pointeur 32 bits supplémentaire qui pointe sur une variable dans laquelle le résultat de la fonction doit être renvoyé.
- Int64 est renvoyé dans EDX:EAX.
- Les résultats pointeur, classe, référence de classe et pointeur de procédure sont renvoyés dans EAX.
- Pour les résultats tableau statique, enregistrement et ensemble, si la valeur occupe un octet, elle est retournée dans AL. Si la valeur occupe deux octets, elle est renvoyée dans AX, et si la valeur occupe quatre octets, elle est renvoyée dans EAX. Sinon le résultat est renvoyé dans un paramètre var supplémentaire et transmis à la fonction après les paramètres déclarés.
Gestion des appels de méthodes
Les méthodes emploient les mêmes conventions d'appel que les procédures et les fonctions ordinaires. Cependant, elles disposent d'un paramètre implicite supplémentaire nommé Self, qui correspond à une référence sur la classe ou l'instance dans laquelle la méthode a été appelée. Le paramètre Self est transmis sous la forme d'un pointeur de 32 bits.
- Avec la convention register, Self se comporte comme s'il avait été déclaré avant tous les autres paramètres. Il est par conséquent toujours transmis dans le registre EAX.
- Avec la convention pascal, Self se comporte comme s'il avait été déclaré après tous les autres paramètres (y compris le paramètre supplémentaire var qui peut être transmis pour obtenir un résultat de fonction). Il est donc toujours empilé en dernier, se terminant à une adresse inférieure à celle de tous les autres paramètres.
- Pour les conventions cdecl, stdcall et safecall, Self se comporte comme s'il avait été déclaré avant tous les autres paramètres, mais après le paramètre supplémentaire var qui doit être transmis pour obtenir un résultat de fonction. Il est donc toujours empilé en dernier, mais avant le paramètre supplémentaire var.
Les constructeurs et les destructeurs utilisent les mêmes conventions d'appel que les autres méthodes, si ce n'est qu'un paramètre indicateur Boolean est transmis pour indiquer le contexte de l'appel constructeur ou destructeur.
Une valeur False du paramètre indicateur dans un appel constructeur indique que ce dernier a été appelé via une instance d'objet ou avec le mot-clé inherited. Dans ce cas, le comportement du constructeur est celui d'une méthode ordinaire. Une valeur True du paramètre indicateur dans un appel constructeur indique que ce dernier a été appelé via une référence de classe. Dans ce cas, le constructeur crée une instance de la classe fournie par Self, et renvoie une référence sur l'objet nouvellement créé dans EAX.
Une valeur False du paramètre indicateur dans un appel destructeur indique que ce dernier a été appelé en utilisant le mot-clé inherited. Dans ce cas, le comportement du destructeur est celui d'une méthode ordinaire. Une valeur True dans le paramètre indicateur dans un appel destructeur indique que ce dernier a été appelé via une instance d'objet. Dans ce cas, le destructeur désalloue l'instance donnée par Self avant de rendre la main.
Le paramètre indicateur se comporte comme s'il avait été déclaré avant tous les autres paramètres. Avec la convention register, l'indicateur est transmis dans le registre DL. Avec la convention pascal, l'indicateur est toujours empilé avant tous les autres paramètres. Avec les conventions cdecl, stdcall et safecall, l'indicateur est toujours empilé juste devant le paramètre Self.
Comme le registre DL indique si le constructeur ou le destructeur est à l'extrémité de la pile d'appels, vous devez restaurer la valeur de DL avant de sortir pour que BeforeDestruction ou AfterConstruction puissent être appelés correctement.
Présentation des procédures de sortie
Les procédures de sortie garantissent que des actions spécifiques sont effectuées (par exemple l'enregistrement et la fermeture de fichiers) avant l'arrêt d'un programme. La variable pointeur ExitProc vous permet d'installer une procédure de sortie afin qu'elle soit systématiquement appelée lors de l'arrêt du programme et ce, que l'arrêt soit normal, forcé par l'appel de Halt, ou le résultat d'une erreur d'exécution. Une procédure de sortie n'attend aucun paramètre.
Remarque : Pour définir des comportements de sortie, il est conseillé d'utiliser les sections de finalisation plutôt que les procédures de sortie. Les procédures de sortie ne sont disponibles que pour les exécutables. Pour les DLL (Win32) vous pouvez utiliser une variable similaire, DllProc, qui est appelée quand la bibliothèque est chargée ou déchargée. Pour les packages, le comportement de sortie doit être implémenté dans la section finalisation. Toutes les procédures de sortie sont appelées avant les sections finalisation.
Les unités peuvent, tout comme les programmes, installer des procédures de sortie. Une unité peut installer une procédure de sortie dans son code d'initialisation et se reposer sur la procédure pour fermer les fichiers ou accomplir d'autres opérations de remise en ordre.
Quand elle est correctement implémentée, une procédure de sortie fait partie d'une chaîne de procédures de sortie. Les procédures sont exécutées dans l'ordre inverse de celui de leur installation, ce qui garantit que le code de sortie d'une unité n'est pas exécuté avant celui des unités qui dépendent d'elle. Pour conserver la chaîne intacte, vous devez enregistrer la valeur en cours de ExitProc avant de lui affecter l'adresse de votre propre procédure de sortie. Ainsi, la première instruction de votre procédure de sortie peut réinstaller la valeur enregistrée de ExitProc.
Le code suivant illustre le squelette de l'implémentation d'une procédure de sortie :
var
ExitSave: Pointer;
procedure MyExit;
begin
ExitProc := ExitSave; // always restore old vector first
.
.
.
end;
begin
ExitSave := ExitProc;
ExitProc := @MyExit;
.
.
.
end.
A l'arrivée, le code enregistre le contenu de ExitProc dans ExitSave, puis installe la procédure MyExit. Quand elle est appelée lors de l'arrêt du programme, MyExit commence par réinstaller la précédente procédure de sortie.
La routine de terminaison de programme de la bibliothèque d'exécution continue à appeler les procédures de sortie jusqu'à ce que ExitProc prenne la valeur nil. Afin d'éviter des boucles infinies, ExitProc est initialisée à nil avant chaque appel, de manière à ce que la procédure de sortie suivante ne soit appelée que si la procédure de sortie en cours assigne réellement une adresse à ExitProc. Si une erreur survient pendant l'exécution d'une procédure de sortie, celle-ci n'est plus appelée de nouveau.
Pour connaître la cause de la terminaison du programme, la procédure de sortie peut lire la variable entière ExitCode et la variable pointeur ErrorAddr. En cas de terminaison normale, ExitCode vaut zéro et ErrorAddr vaut nil. Dans le cas d'un arrêt provoqué par un appel à Halt, ExitCode contient la valeur transmise à Halt et ErrorAddr vaut nil. Enfin, si l'arrêt du programme est consécutif à une erreur d'exécution, ExitCode reçoit le code d'erreur et ErrorAddr l'adresse de l'instruction incorrecte.
La dernière procédure de sortie (celle installée par la bibliothèque d'exécution) ferme les fichiers Input et Output. Si ErrorAddr est différent de nil, elle génère un message d'erreur. Pour afficher vous-même les messages d'erreur d'exécution, installez une procédure de sortie qui lit le contenu de ErrorAddr et affiche un message s'il n'est pas à nil. De plus, avant la sortie de cette procédure, affectez la valeur nil à ErrorAddr afin que l'erreur éventuelle ne soit pas à nouveau signalée par d'autres procédures de sortie.
Après avoir appelé toutes les procédures de sortie, la bibliothèque d'exécution rend la main au système d'exploitation et renvoie la valeur contenue dans ExitCode.