begin process at 2012 02 11 16:14:11
  Trouver un code source :
 
dans
 
Accueil > 

Tutoriels

 > 

Système

 > LES POINTEURS EN DELPHI

LES POINTEURS EN DELPHI


 Information sur le tutoriel

Note :
Aucune note

 Description

Les pointeurs et leur utilisation en Delphi, avec une petite extrapolation sur la communication inter-processus (mémoire partagée) et sur les API ... Ainsi que sur Scanline.

Cordialement, Bacterius !

Tutorial

  Les Pointeurs

Delphi



Bonjour, et bienvenue dans ce tutoriel concernant les pointeurs. Ce tutoriel vous permettra, je l'espère, de mieux comprendre les pointeurs et la façon dont le système Windows gère la mémoire. Nous commencerons par une introduction sur les pointeurs, leur définition, puis nous attaquerons les chapitres pas-à-pas avec des exemples pour bien comprendre le fonctionnement des pointeurs et leur gestion avec Delphi.



  1. Introduction



Dans le sens informatique du terme, un pointeur est un nombre entier sur 32 bits (4 octets) qui représente une adresse mémoire. Une variable est une certaine portion de mémoire qui contient des informations.

Premier danger à l'horizon, il ne faut pas confondre mémoire locale et mémoire globale. La mémoire locale est une certaine quantité de mémoire qui est dédiée à un processus unique. La mémoire globale représente toute la mémoire du système, qui est composée de certains blocs de mémoire locale, et de certains blocs non utilisés. Cependant, un grand avantage des pointeurs est qu'ils peuvent accéder à la mémoire globale, alors que les variables objet (TBitmap, Integer, ...) sont stockées automatiquement dans la mémoire locale du processus. Néanmoins, il faut avoir l'autorisation d'accéder à un endroit de la mémoire avant de même songer à y toucher (c'est là l'origine des violations d'accès, entre autres ...).

Ce texte sera le fil conducteur de tout le tutoriel :

Représentons la mémoire comme une ville, avec des maisons de taille différente, qui représentent des portions de mémoire, des variables. Les quartiers de la ville sont les processus, imaginons qu'on ne puisse pas changer de quartier sans avoir une autorisation de la mairie de notre quartier. Je suis un nouvel habitant, j'ai acheté une maison de taille 2 octets sur 2 octets. J'ai donc une portion de mémoire de 4 octets, et j'ai une adresse sur ma boîte aux lettres. Supposons que mon adresse soit 123. Je n'ai pas le droit de changer cette adresse, mais j'ai le droit de changer la décoration de ma maison. Je représente la décoration de ma maison par un nombre de 4 octets (de la taille de ma maison en fait). En arrivant, j'ai envie de mettre la déco à la valeur 736. Puis, en me promenant dans la rue, je change d'avis. Vite, j'arrive devant ma maison, je vois mon adresse, et j'ouvre la porte. C'est le principe du déréférencement (c'est-à-dire accéder à une variable à partir de son adresse). Je change ma déco à la valeur 12. Puis, je décide d'aller voir mon ami qui se trouve dans le quartier voisin. Une fois arrivé à la séparation des deux quartiers, on me dit que je suis un étranger et que les étrangers sont interdits. Vexé, je contacte mon ami par téléphone (ils seront symbolisés par les messages Windows). Il me dit par téléphone les détails de sa décoration, en m'indiquant bien qu'il a une plus grande maison que la mienne (16 octets). Je lui réponds que je n'ai pas le droit d'aller chez lui, et lui n'a pas le droit d'aller chez moi, et je lui fais part d'une idée pour pouvoir se transmettre des informations facilement. Parce que le téléphone, ça n'est pas sans risque : nos conversations peuvent être écoutées ... Si je bâtissais, dans un coin reculé de la ville, une petite cabane de 4 octets, sous aucune autorité, où mon ami et moi pourront échanger nos codes de décoration. J'irai dans la cabane, je déposerai une décoration dedans, j'appellerai mon ami pour lui dire d'aller voir ma nouvelle décoration dans la cabane, comme ça il pourra changer la sienne lui-même. Évidemment, nous aurons tous deux une clé pour rentrer dans la cabane, comme ça nous seuls pourrons rentrer dans la cabane. (La clé représente l'adresse de la cabane, ici).

Autant de thèmes que nous allons aborder dans ce tutoriel :

  • Création et libération de pointeurs

  • Variables et pointeurs

  • Accès à la mémoire d'un autre processus (mémoire partagée)



  1. Création et libération de pointeurs



  • Notion de pointeur dans Delphi

Dans Delphi, un pointeur est d'abord de type pointer. Mais ce pointer tel qu'il est ne sert pas à grand-chose, car il ne pointe sur aucun type précis. Il faut donc préciser le type du pointeur. Il existe quelques types de pointeur prédéfinis, comme le pointeur qui pointe sur un byte (PByte), un mot, ou word (PWord), un double mot, ou dword (PDWord). Notez que par convention, on rajoute (ou on remplace le T par) un P pour indiquer qu'il s'agit d'un pointeur. Vous pouvez créer vos propres types de pointeur, suivant ce modèle :

type
 TMonType = record
  Titre: String[255];
  Numero: Integer;
 end;
 { Imaginons un enregistrement quelconque ... }

 PMonType = ^TMonType;
 { L'accent circonflexe devant le nom du type déclare un type de pointeur qui pointera sur ce type }

Vocabulaire : un pointeur qui ne pointe sur aucun type est appelé un pointeur non typé, en Delphi.

  • Utilisation du pointeur dans Delphi

Tout pointeur que vous créerez sera simplement une adresse 32 bits (4 octets). Vous pouvez choisir de laisser Delphi choisir un emplacement de mémoire libre pour le pointeur, ou vous pouvez choisir vous-même l'adresse du pointeur (attention, vous devrez vous assurer que vous avez le droit d'accéder à cette adresse). Soit le code suivant :

var
 Pointeur: PByte; { PByte est un pointeur qui pointe vers un emplacement mémoire 1 octet }
begin
 New(Pointeur); { On laisse Delphi choisir un emplacement de mémoire libre }
 Pointeur^ := 45; { L'emplacement de mémoire contient à présent la valeur 45 }
 { On utilise ^ pour indiquer qu'on manipule l'emplacement pointé par le pointeur et non pas le pointeur }
 Dispose(Pointeur); { On libère le pointeur et la mémoire sur lequel il pointait }

 Pointeur := Ptr(6434); { On fait pointer notre pointeur vers l'emplacement 6434 de la mémoire }
 { Vous n'aurez probablement pas le droit d'accéder à cet emplacement }
 Pointeur^ := 22; { On change la valeur, mais il est très probable que ça ne fonctionne pas ... }
 Dispose(Pointeur); { Dans ces circonstances, il ne faut pas appeler Dispose, car cela libérerait l'emplacement 6434, qui contient peut-être des données très importantes (dans ce cas, Delphi lèvera une exception vous indiquant que vous n'avez pas le droit d'accéder à ces données) }
end;

Comme vous l'aurez constaté, il y a plus de précautions à prendre lorsque l'on choisit soit-même son adresse. En fait, on ne la choisit même pas. En général, on fait pointer un pointeur sur une variable qui existe déjà en mémoire. On laissera généralement Delphi créer et libérer la mémoire pour nous, sauf dans de rares exceptions très particulières.

Vocabulaire : quand on affecte une adresse à un pointeur, on dit qu'on l'initialise. On peut initialiser en utilisant New, ou en faisant pointer le pointeur sur une variable existante.

  • Déréférencement

Ce mot barbare porte bien son nom, car il a pour effet d'accéder à une variable plutôt qu'à sa référence (le pointeur qui pointe dessus). On parle donc de déréférencement quand on veut accéder aux données sur lesquelles pointe notre pointeur. Il est très simple de déréférencer le pointeur en Delphi, puisqu'il suffit d'ajouter un accent circonflexe après le pointeur, comme ceci :

var
 Pointeur: PByte;
begin
 New(Pointeur); { On crée notre pointeur et la variable sur laquelle il pointe }
 Pointeur^ := 63; { On déréférence le pointeur pour que les données sur lesquelles il pointe prennent la valeur 63 }
 Dispose(Pointeur); { On libère le tout }
end;

Une fois le pointeur déréférencé, traitez-le comme s'il s'agissait d'une variable du type sur lequel le pointeur pointe.

Attention : déréférencer un pointeur de type pointer n'a pas de sens car il ne pointe sur aucun type, autrement dit, le compilateur ne sait pas ce qu'il faut stocker à l'adresse du pointeur. Delphi déclenchera éventuellement une erreur de compilation si vous tentez de déréférencer un pointeur non typé. Déréférencer un pointeur avant de l'initialiser ne provoque pas d'erreur de compilation (peut-être un avertissement) mais causera une erreur à l'exécution. De même, tenter de déréférencer un pointeur qui pointe sur une variable inexistante ou interdite d'accès provoquera aussi une erreur d'exécution, ou, dans le meilleur des cas, stocker (ou lire) une valeur nulle dans la variable.

  • Résumé

Dans ce chapitre, vous aurez appris comment créer et libérer un pointeur, vous aurez acquis la notion de pointeur et de type de pointeur, et vous saurez comment déréférencer votre pointeur.
Dans le chapitre suivant, nous approfondirons le lien qui existe entre variable et pointeur.



  1. Variables et pointeurs



  • La variable

Nous avons vu dans le précédent chapitre les bases sur les pointeurs en Delphi. Nous allons maintenant approfondir la relation qui unit la variable et le pointeur, après avoir défini le terme de variable.
Une variable est une portion de mémoire dans la mémoire globale. Elle est définie par un début, une taille, et peut être associée à un ou plusieurs pointeurs, qui représentent le début de la variable, et qui peuvent spécifier implicitement la taille de la variable s'ils sont typés (un pointeur non typé n'est associé à aucun type de variable précis, il ne véhicule donc pas d'information sur la taille de la variable sur laquelle il pointe). Voici un schéma pour mieux comprendre :

 

Image1.bmp


Note : la variable commence au début de l'octet 12 et se termine au début de l'octet 16 (le schéma peut prêter à confusion).

Ici, le pointeur est de type mot double, le compilateur sait que la variable est donc de type mot double, et que donc la longueur de la variable est de 4 octets. Notez que Delphi autorise que certaines variables comment à la même adresse mémoire que d'autres (à l'aide de la directive absolute, voir l'aide de Delphi). Dans ce cas, les variables partageront la même valeur si elles sont du même type. Si elles sont de type et de taille différentes, les octets de la plus petite variable seront les mêmes que les premiers octets de la plus grande variable. Par exemple, si l'on fait commencer une variable W de type mot (Word) de 2 octets à la même adresse qu'une variable DW de type double mot (DWord) de 4 octets, les 2 octets de W (la totalité de la variable) seront les mêmes que les 2 premiers octets de DW (mais les 2 octets restants de DW n'auront aucun rapport avec W). Notez que l'on ne peut pas modifier une variable suivie de la directive absolute. Par exemple, dans l'exemple suivant, vous ne pouvez pas affecter de valeur à DW mais seulement la lire ! Voici un exemple de l'utilisation de la directive absolute :

var
 W: Word;
 DW: DWORD absolute W; { DW commence à la même adresse que W }
begin
 { Il est impossible de changer DW, on ne peut faire que lire ... }
 W := 8374; { Les 2 premiers octets de DW correspondront aux 2 octets de W }
end;

Notez que vous pouvez faire pointer plusieurs pointeurs sur une même variable. Ce procédé est peu utile dans le cadre d'un seul processus, mais se révèle très intéressant dans le cas de mémoire partagée (nous aborderons la notion de mémoire partagée plus tard dans le tutoriel).

  • Scanline et les pointeurs

Vous connaissez peut-être Scanline, une propriété des bitmaps Delphi. Ils permettent d'obtenir un accès direct à la mémoire où est stocké le bitmap. D'abord, n'oublions pas qu'un bitmap est stocké à l'envers dans un fichier (la ligne 1 passe en dernier, la ligne 2 en avant-dernier ...). Ce qui fait que l'ordre des pixels est différent.
Ensuite, la mémoire du bitmap est faite de telle sorte qu'il s'agit d'une suite de "paquets", ou chaque paquet représente un pixel. La taille du paquet varie : par exemple, pour un bitmap de format 8 bits, chaque paquet sera de 1 octet, alors que pour un bitmap de format 32 bits, chaque paquet sera de 4 octets. Ceci est très important, car avec Scanline vous aurez accès, bien sûr, à des pointeurs sur la mémoire du bitmap, mais il faudra savoir les interprêter. Ici, nous prendrons le cas d'un bitmap 32 bits (le plus fréquent avec 24 bits). Imaginons un bitmap de 3 pixels sur 3 pixels, qui alterne le pixel rouge (R) et le pixel noir(N). Nous aurions alors un bitmap de ce type :

RNR
NRN
RNR

Nous avons vu précédemment que l'ordre des pointeurs est inversé, mais ici cela a peu d'importance vu que le bitmap est symétrique par rapport à l'axe horizontal passant par NRN.

Quand nous utilisons Scanline, il nous donne un pointeur qui pointe sur le premier pixel de la ligne choisie. Par exemple, Scanline[0] (première ligne) nous donnera un pointeur sur le pixel R en bas à gauche (n'oubliez pas que les lignes sont inversées !), et Scanline[2] nous donnera un pointeur sur le pixel R en haut à gauche. Le problème, c'est que Scanline renvoie un pointeur non typé (Pointer). Il faut donc savoir ce qu'il représente. Si notre bitmap est en 32 bits, Scanline renvoie un pointeur sur un pixel de 4 octets. Dans un bitmap 32 bits, la structure d'un pixel est en fait un enregistrement de 4 octets qui donnent respectivement la valeur R, G, B de la couleur du pixel, ainsi qu'un octet réservé. Voici sa structure :

type
 TRGBQUAD = record
  rgbBlue: Byte;
  rgbGreen: Byte;
  rgbRed: Byte;
  rgbReserved: Byte;
 end;

Remarque : dans ce tutoriel, on notera ces composantes B (blue), G (green), R (red) et A (pour le reservé, mais souvent utilisé comme canal alpha).

Seulement, il s'agit d'un enregistrement, et non pas d'un pointeur. Ici, pas de problème, il suffit de créer un type de pointeur qui type sur cet enregistrement. Delphi nous en fournit un : PRGBQUAD.
Nous allons utiliser ceci pour interpréter les données de Scanline. N'oubliez pas que la mémoire d'un bitmap est une suite d'octets, il est donc possible de lire chaque pixel l'un après l'autre comme on ferait avec un fichier. La technique de lecture illustrée ici, permet de n'utiliser qu'un seul appel à Scanline pour avoir le premier pixel du bitmap, puis de traverser intégralement la mémoire du bitmap en incrémentant le pointeur à chaque nouveau pixel. Il existe bien sûr d'autres techniques qui impliquent parfois l'appel à Scanline pour chaque nouvelle ligne. Je dirais bien sûr qu'un seul appel à Scanline est bon pour les performances ! Voici maintenant comment utiliser cette technique. Notons notre bitmap « Bitmap », et gardons le bitmap que nous avions pris au début (alternance pixel rouge/pixel noir).

Dans cet exemple, nous allons permuter les pixels noirs et les pixels rouges de notre bitmap (si il est rouge, il passe noir, et vice-versa) :

var
 Pix: PRGBQUAD; { Le pixel qu'on est en train de traiter }
 X, Y: Integer; { Les coordonnées }
begin
 Pix := Bitmap.Scanline[Bitmap.Height - 1];
 { On prend le premier pixel du bitmap (premier pixel de la dernière ligne, donc. }
 for X := 0 to Bitmap.Width - 1 do
  for Y := 0 to Bitmap.Height - 1 do
   begin
    if Pix^.rgbRed = 255 then Pix^.rgbRed := 0 else Pix^.rgbRed := 255; { Si la composante rouge est 255, le pixel est rouge (dans notre exemple), sinon, le pixel est noir. On permute en conséquence. }
    Inc(Pix); { On passe au pixel suivant }
   end;
end;

Vous n'aurez qu'à comparer les performances avec les autres techniques (Scanline ou pas).

Attention toutefois à toujours vérifier le format des pixels du bitmap, pour éviter des erreurs de pointeurs désastreuses ! Si votre bitmap est en 24 bits, par exemple, il convient alors d'utiliser PRGBTRIPLE pour un pointeur vers un pixel du bitmap, et non plus PRGBQUAD !
Un exemple d'une erreur regrettable d'un développeur inattentif (le bitmap est en 24 bits) :

- Structure mémoire du bitmap

Pixel 1 Pixel 2 Pixel 3 ...

 BGR   BGR   BGR ...

Oui les composantes sont aussi inversées : BGR et non pas RGB ! Mais les types TRGBTRIPLE et TRGBQUAD s'en occupent pour vous.

- Comment le développeur va lire ce bitmap 24 bits en utilisant un pointeur pour bitmap 32 bits :

(rappellons que la taille d'un pixel 32 bits est de 4 octets, contre 3 octets pour un 24 bits)

Pixel 1 Pixel 2 Pixel 3

 B G R   BGR   BGR

 | | |   | | |   | | |

 B G R   ABG   R A B

On constate tout de suite l'aberration révélée par ce schéma : le premier pixel (pour notre développeur) empiétera sur le 2eme pixel (pour le bitmap), et ceci est dû à la différence de taille des pixels. L'erreur est d'autant plus grave qu'à la fin du bitmap (supposons qu'il se termine au pixel 211) :

Pixel 210 Pixel 211

  B G R     BGR

  | | |     | | |

  B G R     ABGR A

Le dernier pixel 211 du bitmap n'est pas le dernier pixel pour le développeur (à cause du décalage constant entraîné, ce développeur n'est probablement qu'au 158ème pixel), et il va donc continuer à lire le bitmap. Regardez le dernier pixel (pour le programmeur, ligne du bas) de ce schéma : les composantes R et A (ainsi que tous les pixels suivants, pour le développeur) dépassent de la mémoire du bitmap ! Violation d'accès garantie.
C'est pourquoi je vous invite à toujours vérifier le format du bitmap et à prendre le bon type de pointeurs pour le bon type de pixels ...

Autres utilisations possibles des pointeurs et des variables

N'oubliez pas qu'une variable reste une suite de nombres codés sur 1 octet du point de vue de la mémoire, et que par conséquent vous pouvez écrire un nombre de 4 octets dans une variable de type chaîne de 4 caractères. Voici un exemple de cette méthode :

var
 MaChaine: array [0..3] of Char; { Un char est une valeur ASCII, codée sur un octet }
 Pointeur: PDWORD; { PDWORD est le pointeur de type double mot (DWORD) }
begin
 MaChaine := 'Text'; { On donne une valeur quelconque à la chaîne ... }
 Pointeur := @MaChaine; { On fait pointer Pointeur sur la chaîne (la chaîne a une taille de 4 octets, et le pointeur est du type 4 octets, donc le pointeur englobe toute la chaîne }
 Pointeur^ := 65; { MaChaine a pris la valeur 65, donc le premier caractère devrait être un 'A' }
 ShowMessage(MaChaine); { On affiche la chaîne pour voir ce qui a changé }
 { Ne pas libérer Pointeur car nous avons pris l'adresse de MaChaine, et nous n'avons pas à nous occuper de sa libération. Éventuellement, mettez Pointeur à nil si vous préférez }

end;

Notez qu'on aurait pu faire la même chose en affectant le transtypage du nombre 65 en char à MaChaine. Il s'avère que dans certains cas il n'est pas nécessaire de passer par les pointeurs. Mais parfois, vous devrez les manipuler.

  • Dans le cas des API Windows

Qu'est-ce qu'une API ? C'est une routine exportée, souvent contenue dans une DLL, qui permet d'effectuer des opérations de bas niveau qui touchent au système ou aux périphériques. Certaines sont assez difficiles d'utilisation, d'autres sont simples et très utiles. Vous en avez sûrement déjà utilisées : GetCursorPos, SetCursorPos sont des API, par exemple. Elles sont la base du système d'exploitation, ce sont les fonctions de base pour créer une application (par exemple, quand vous créez, ne serait-ce qu'une fiche, dans Delphi, des dizaines d'API sont appelées pour la créer). La VCL de Delphi ne fait qu'envelopper ces API pour en proposer un accès plus simple et plus rapide.
API signifie Application Programming Interface (littéralement : Interface de Programmation d'Application).
Comme vous le savez peut-être, les API Windows font une utilisation abusive des pointeurs pour qu'on leur transmette des informations, ou encore pour qu'elles nous renvoient des informations.
Vocabulaire : PChar est un type très utilisé dans Windows. Il s'agit d'un pointeur sur une chaîne de longueur quelconque, qui se termine toujours par le caractère nul (#0) - les fonctions utilisant ce type vont parcourir le tableau de caractères jusqu'à trouver le caractère nul qui marquera la fin de la chaîne. Ce type est aussi appelé « chaîne à zéro terminal ».
Prenons un exemple simple, GetComputerName, qui renvoie le nom de l'ordinateur sur lequel vous vous trouvez. Un développeur débutant serait tenté de faire ceci :

var
 S: String;
 Size: Cardinal;
begin
 Size := 255;
 GetComputerName(PChar(S), Size);
 ShowMessage(S);
end;

Pourquoi ce code ne peut-il pas fonctionner ? Tout simplement parce que, lors de l'appel à GetComputerName, ce développeur transtype sa variable String en PChar, ce qui a pour effet de passer un pointeur sur S à la fonction. Or, on ne connaît pas l'adresse du pointeur qui a été envoyé à GetComputerName, mais la fonction va tout de même tenter d'écrire dedans. Evidemment, cela va déclencher une exception car PChar(S) va pointer sur le premier caractère de S. Or, le type String de Delphi garde le premier caractère de la chaîne comme indicateur de la taille du tableau, et refuse tout accès à cette valeur. Quand la fonction va écrire dans le pointeur PChar(S), elle va écrire par-dessus le premier caractère, ce qui va causer énormément de problèmes. Comment remédier à ce problème ? En créant nous-mêmes notre variable PChar, et en la passant nous-mêmes à la fonction. Notez que New ne suffira pas pour un PChar, car il faut déjà déterminer la taille de la variable au départ. Nous utiliserons donc les fonctions StrAlloc et StrDispose (enfin, vous pouvez appeler New, et ensuite définir la taille de la variable, mais il est plus rapide et plus sûr d'appeller StrAlloc).
Au passage, le deuxième paramètre de GetComputerName doit contenir la longueur maximale du nom de l'ordinateur. Il est défini par Windows à MAX_COMPUTERNAME_LENGTH, nous serons donc tentés de fixer la taille de notre chaîne à cette constante, plus un caractère pour contenir le caractère nul à la fin (il est très important !). Voici comment procéder :

var
 S: PChar;
 Size: Cardinal;
begin
 S := StrAlloc(MAX_COMPUTERNAME_LENGTH + 1);
 Size := MAX_COMPUTERNAME_LENGTH + 1;
 GetComputerName(S, Size);
 ShowMessage(S);
 StrDispose(S);
end;

De cette façon, cela fonctionne. Nous voyons avec bonheur le petit message contenant le nom de notre ordinateur ...
Remarquez que GetComputerName a bien modifié la variable située à l'adresse pointée par PChar, mais il a aussi modifié une autre variable : celle que nous avons passé en deuxième paramètre (Size). La fonction écrit normalement dans cette variable le nombre de caractères écrits dans S (caractère nul non compris). Par exemple, si votre nom d'ordinateur a une longueur de 12 caractères, Size contiendra 12 après la fonction. Vous pouvez vérifier !
Notez également que l'on a pas vérifié la valeur de retour de GetComputerName car c'est un exemple. Mais normalement, il faut vérifier si la fonction a réussi à opérer sur les variables, et eventuellement vérifier le dernier code d'erreur (avec GetLastError) pour voir ce qui s'est passé. C'est très utile pour savoir pourquoi notre code ne fonctionne pas.

Cependant, certaines API plus complexes ont besoin de pointeurs vers des structures plus complexes que des chaînes. Quand vous devez passer un pointeur vers un tableau d'éléments, passez un pointeur pointant vers le premier élément du tableau. Normalement, l'API vous demandera également la taille du tableau, et elle sera ainsi capable de lire tout le tableau (elle en connait le début et la taille).

Remarque : quand vous passez une variable à une fonction API, trois cas se présentent :
- l'API va seulement lire la valeur : vous pouvez passer une constante ou un transtypage de variable (ou une variable, évidemment) compatible avec le type requis par l'API.
- l'API va écrire la valeur : vous devez passer une variable du même type que celui requis par l'API.
- l'API va lire la valeur, puis écrire dedans : vous devez faire comme dans le cas précédent, mais en initialisant la variable avant.

  • Résumé

Je pense avoir tout dit sur les variables et les pointeurs dans ce chapitre. Vous devriez maintenant savoir définir exactement une variable, comment modifier une variable par le biais des pointeurs, et savoir utiliser correctement une chaîne à zéro terminal (PChar) dans les appels à des API.
Dernière remarque pour ce chapitre : Delphi peut transtyper un PChar en String automatiquement, mais ne peut pas faire l'inverse. Vous pouvez alors utiliser une variable de type PChar dans des fonctions demandant des variables de type String, mais pas le contraire (vous devrez procéder comme dans l'exemple précédent, ou transtyper, si la valeur doit seulement être lue). En règle générale, le transtypage fonctionne uniquement pour la lecture. Pour l'écriture, il faudra créer des variables du même type que celui dans lequel on va écrire (comme dans l'exemple précédent).

  1. Accès à la mémoire d'un autre processus

  • La mémoire partagée

La mémoire partagée est une branche de l' IPC (Inter-Process Communication, ou Communication Inter-Processus), qui permet à deux processus distincts d'accéder au même emplacement mémoire. Elle peut se gérer de différentes manières, nous aborderons ici le cas d'un fichier paginé.

Tout d'abord, qu'est-ce que la mémoire partagée, exactement : c'est une portion de mémoire qui n'appartient à aucun processus précis, et qui peut être accédée par n'importe-quel processus s'il a le droit d'y accéder. Ensuite, qu'est-ce qu'un fichier paginé : il s'agit d'un fichier qui est contenu dans le fichier de pagination Windows (Pagefile.sys). Ce dernier est dans la continuité de la mémoire vive : en effet, peu d'ordinateurs ont assez de mémoire vive pour gérer toutes les applications qui tournent simultanément sur leur système. Windows a donc été obligé d'introduire un nouveau concept : faire usage du disque dur pour y stocker de la mémoire. Concept interessant, certes, mais très lent : en effet, un accès au disque est beaucoup plus long qu'un accès à la mémoire, c'est pourquoi il faut user de cet outil avec parcimonie. Certaines API de Windows sont dédiées à la création de fichiers paginés, qui n'existent qu'en mémoire - une fois que tous les processus qui utilisaient ce fichier paginé l'ont libéré, il est automatiquement effacé. En fait, les API CreateFileMapping, MapViewOfFile, UnMapViewOfFile et CloseHandle peuvent être utilisées pour créer un fichier paginé. Je ne développerai pas ici l'utilisation exacte de ces API, référez-vous à la MSDN pour plus d'informations.

Notez que la mémoire partagée peut se faire aussi bien dans un fichier paginé que dans un fichier physique (qui existe réellement sur le disque). Il sera alors conservé même à la fermeture du dernier processus l'utilisant. Il suffit de changer quelques paramètres dans les appels aux API précédentes.

  • CreateFileMapping

Cette fonction crée un fichier et le mappe (le stocke en mémoire). Pour créer un fichier paginé, il suffit de définir le premier paramètre (hFile) à INVALID_HANDLE_VALUE, et le 3ème paramètre à PAGE_READWRITE. La taille du fichier paginé doit être stockée dans dwMaximumSizeLow, et le nom du fichier doit être stocké dans lpName (vous pouvez transtyper ici, la valeur est seulement lue). Ce nom de fichier doit respecter les conventions habituelles (pas de slashs, etc ...), mais, dans le cas d'un fichier paginé, n'existe pas nécessairement sur le disque : vous pouvez par exemple créer un fichier paginé avec le nom « Salut » sans problème. Conservez la valeur de retour, il s'agit du descripteur du fichier paginé.

Chaque processus devra utiliser cette fonction pour accéder au fichier paginé. Le premier processus créera le fichier paginé, les autres recevront simplement le descripteur du fichier paginé sans le recréer.

  • MapViewOfFile

Cette fonction mappe le fichier paginé dans le processus qui appelle la fonction, c'est-à-dire qu'elle renvoie un pointeur sur le fichier paginé. Passez en paramètre le descripteur du fichier paginé (dans hFileMappingObject), et la constante FILE_MAP_ALL_ACCESS dans dwDesiredAccess. Les autres paramètres peuvent être nuls, sauf si vous désirez faire une utilisation plus approfondie de cette fonction. Gardez précieusement la valeur de retour, il s'agit d'un pointeur sur le début du fichier paginé dans la mémoire. Vous pouvez à partir de ce pointeur lire et écrire dans le fichier paginé, et les modifications seront immédiatement visibles par les autres processus. Souvenez-vous que votre fichier a une taille, et qu'il ne faut pas la dépasser.

Chaque processus devra utiliser cette fonction pour pouvoir accéder au fichier paginé en mémoire.

Note : Le pointeur renvoyé par cette fonction étant non typé, il faudra soit le convertir durablement en pointeur typé si vous devez tout le temps écrire la même structure de données dans le fichier, ou bien effectuer un copiage de mémoire avec CopyMemory à chaque écriture/lecture dans le cas contraire.

  • UnMapViewOfFile

Cette fonction détache le processus du fichier paginé. Passez en paramètre le pointeur vers le fichier paginé. Après cette fonction, vous ne devez plus utiliser le pointeur reçu à l'appel de MapViewOfFile.

A la fin de l'utilisation du fichier paginé, pensez bien à utiliser cette fonction pour indiquer au système d'exploitation qu'il faut éventuellement effacer le fichier paginé si vous êtes le dernier processus à utiliser le fichier.

  • CloseHandle

Cette fonction ferme le fichier paginé pour le processus qui appelle la fonction.

Pensez bien à appeler cette fonction pour libérer les ressources associées au descripteur du fichier paginé.

  • Synchronisation

Vous vous dites sûrement que c'était plus simple que vous ne pensiez de communiquer entre deux processus. Mais il y a un sérieux problème ! Imaginez si deux processus accèdent au fichier paginé en même temps ? Nous pourrions avoir des problèmes de violations d'accès, ou au mieux une incohérence des données. C'est pourquoi il faut utiliser la synchronisation des appels au fichier paginé. La plupart des développeurs utilisent les mutex (ou dans certains cas les sémaphores) pour contrôler l'accès aux données. Créez votre mutex avec CreateMutex, et libérez-le avec ReleaseMutex suivi de CloseHandle. Pour synchroniser, il suffit, dans chaque processus, de tenter de créer le mutex. Si la création échoue (et si GetLastError = ERROR_ALREADY_EXISTS), cela veut dire que le mutex est déjà créé, ce qui signifie implicitement qu'un autre processus écrit ou lit déjà dans le fichier paginé. Il faut donc tenter de créer le mutex jusqu'à ce que la création réussisse : à ce moment-là, nous pouvons écrire ou lire dans le fichier paginé, et libérer le mutex quand nous avons fini, pour laisser le prochain processus écrire à son tour. En règle générale, lisez ou écrivez dans le fichier paginé quand vous avez le mutex. Notez qu'il faudra donner au mutex un nom qui sera unique pour chaque fichier paginé (si vous avez deux fichiers paginés qui utilisent le même mutex d'accès, de nombreuses erreurs peuvent survenir). La meilleure solution consiste à donner au mutex le nom du fichier paginé (eventuellement suivi d'un petit tag).

  • Résumé

La communication inter-processus peut se faire à l'aide de mémoire partagée (qui est un moyen rapide et relativement facile à utiliser), mais peut se faire également à l'aide de pipes (nommées ou non), de sockets Windows , ou encore de messages Windows (le plus simple moyen de communication) . Mais toutes ces méthodes ne seront pas abordées dans ce tutoriel. Nous allons maintenant conclure ce tutoriel sur l'importance des pointeurs dans le système Windows.

  1. Conclusion

Si les pointeurs peuvent paraître rebutants au premier abord, ils sont un élément-clé dans le système d'exploitation Windows , et sont indispensables au bon fonctionnement du système. Ils sont d'une très grande souplesse , et permettent parfois d'accélérer des opérations qui auraient pris beaucoup plus de temps avec des variables. Sachez qu'il vaut toujours mieux travailler avec des pointeurs qu'avec des objets, puisqu'en réalité tous les objets dans Delphi sont des pointeurs , dont l'utilisation est gérée par Delphi (par exemple, Delphi déréférence automatiquement les objets pour nous). De plus, les pointeurs permettent d'accéder à la « couche basse » du système , c'est-à-dire à la mémoire globale, sans être limités par la pile locale du processus.

Cependant, il faut savoir manier les pointeurs de façon prudente , car ils sont aussi fascinants que dangereux, et une mauvaise utilisation des pointeurs, comme une mauvaise utilisation des API, peut causer bien des dommages à votre ordinateur.

Gardez également à l'esprit que la mémoire n'est en réalité qu'une suite d'octets , et que tous les objets que vous manipulez sont en réalité des blocs d'octets que vous manipulez en un seul morceau.



J'espère que ce tutoriel vous a été utile, et que vous en aurez tiré des connaissances supplémentaires, car après tout, c'était son but.
Merci à vous d'avoir pris le temps de lire ce tutoriel.
A bientôt !



Contact : DelphiFr (Bacterius), E-m@il (thomas.beneteau@yahoo.fr).

 Historique

01 avril 2009 19:46:21 :
// Petites rectifications de mise en page (textbox anti-puces ...).
01 avril 2009 19:47:50 :
// Fini !
01 avril 2009 19:49:56 :
// Voilà ...
22 avril 2009 20:19:48 :
// Ajout d'un paragraphe concernant Scanline dans le chapitre Variables et Pointeurs.

Commentaires

Commentaire de yannfrance le 02/04/2009 15:55:56

Bonjour,
Merci pour ton tutoriel.
C'est très clair, bien détailler, bien expliquer en profondeur.
Ca tombe juste au moment ou j'en ai besoin.
Ca fait plaisir, merci bien Bacterius...

Commentaire de Bacterius le 02/04/2009 16:55:21

De rien, ça faisait longtemps que je voulais poster un truc sur les pointeurs ...
Je suis à peu près sûr de tout de ce que je raconte, après on va bien voir ^^

Cordialement, Bacterius !

Commentaire de cirec le 02/04/2009 20:02:38 administrateur CS

tiens regarde on peut aussi écrire ;)

type
  tagBGRQUAD = packed record
    bgrRed: Byte;
    bgrGreen: Byte;
    bgrBlue: Byte;
    bgrReserved: Byte;
  end;
  TBGRQuad = tagBGRQUAD;


procedure TForm1.Button1Click(Sender: TObject);
var
  aColor        : Integer;
  aRGB          : TBGRQuad ABSOLUTE aColor;
begin
  with PaintBox1.Canvas do
  begin
    aColor := Brush.Color;
    with aRGB do
    begin
      bgrRed := 250;
      bgrGreen := 0;
      bgrBlue := 0;
    end;
    Brush.Color := aColor;
    FillRect(PaintBox1.ClientRect);
  end;
end;

Commentaire de Bacterius le 02/04/2009 20:14:46

Tout à fait ça remplace bien la fonction RGB ^^

Cordialement, Bacterius !

Commentaire de Bacterius le 04/04/2009 10:54:10

Euh Cirec ne vaudrait-il pas plutôt faire ceci :

procedure TForm1.Button1Click(Sender: TObject);
var
  aColor        : Integer;
  aRGB          : TBGRQuad ABSOLUTE aColor;
begin
  with PaintBox1.Canvas do
  begin
    with aRGB do
     begin
      bgrRed := 250;
      bgrGreen := 12;
      bgrBlue := 66;
     end;
    { Ici, aColor prend la couleur rgb(250, 12, 66) }
    Brush.Color := aColor;
    FillRect(PaintBox1.ClientRect);
  end;
end;

Commentaire de cantador le 04/04/2009 11:33:46 administrateur CS

Bon travail Bacterius
de la pédagogie et de plus bien écrit
(7 + 1)

Commentaire de cantador le 04/04/2009 11:34:16 administrateur CS

7 + 1

Commentaire de Bacterius le 04/04/2009 11:52:32

Merci Cantador :)

Cordialement, Bacterius !

Commentaire de cantador le 04/04/2009 11:59:16 administrateur CS

Ils sont d'une très grande souplesse, et permettent parfois d'accélérer des opérations qui auraient pris beaucoup plus de temps avec des variables.

Un petit regret cependant que tu n'as pas étendu un peu plus cet aspect..

Commentaire de Bacterius le 04/04/2009 12:15:50

Arf, je vais tenter de développer un peu plus cette idée dès que j'ai fini de coder ma gauge (pour mon nouveau pack de composants).

Cordialement, Bacterius !

Commentaire de cirec le 04/04/2009 14:36:33 administrateur CS

tout dépend de ce que tu veux faire ...
l'exemple (bidon) portait sur la possibilité de récupérer la valeur initial de la couleur dans aColor et de la modifier par aRGB (qui devrait être aBGR ^^)

dans ton exemple on ne peut que modifier la valeur dans son ensemble alors qu'avec le miens on peut la modifier partiellement

pour un autre exemple:
http://www.codyx.org/snippet_exclure-caracteres-chaine_376.aspx#1293
la deuxième fonction.

Commentaire de Bacterius le 04/04/2009 14:45:14

Oui mais avec ton exemple c'était toujours une couleur noire qui sortait ?

Cordialement, Bacterius !

Commentaire de cirec le 04/04/2009 15:31:13 administrateur CS

ah ...pas chez moi !!!!
testé sous D7, TurboDelphi et Delphi2009
et j'utilisai déjà cette méthode sous D4 !!!

Commentaire de Bacterius le 04/04/2009 15:39:57

Ah ba moi chez moi je pose une paintbox, un bouton, et dans le bouton :

type
  tagBGRQUAD = packed record
    bgrRed: Byte;
    bgrGreen: Byte;
    bgrBlue: Byte;
    bgrReserved: Byte;
  end;
  TBGRQuad = tagBGRQUAD;


procedure TForm1.Button1Click(Sender: TObject);
var
  aColor        : Integer;
  aRGB          : TBGRQuad ABSOLUTE aColor;
begin
  with PaintBox1.Canvas do
  begin
    aColor := Brush.Color;
    with aRGB do
    begin
      bgrRed := 250;
      bgrGreen := 0;
      bgrBlue := 0;
    end;
    Brush.Color := aColor;
    FillRect(PaintBox1.ClientRect);
  end;
end;

Et ça fait une paintbox toute noire ^^ quand je clique sur le bouton.

Cordialement, Bacterius !

Commentaire de Caribensila le 10/04/2009 18:35:05

Eh bien ?!
Personne n'a noté cet excellent tuto ?
Même si la perfection n'existe pas en matière de tuto, je trouve que ça vaut bien un 10/10 pour l'effort pédagogique.
Bravo!

PS: Si tu fais une MAJ, une petite illustration avec scanline et ses possibilités dûes aux pointeurs pourrait être intéressant je crois.

Commentaire de Caribensila le 10/04/2009 18:38:42

La note n'est pas prise en compte!  :s

Je repasserai...

Commentaire de Bacterius le 10/04/2009 19:17:43

Je vais bientôt faire une MAJ, dans le courant des vacances normalement (vers mercredi-jeudi) pour ce tutorial.
Merci Cari :)

Cordialement, Bacterius !

Commentaire de kelloucheaeh le 22/04/2009 10:11:53

salut
je n'ai pas encore lu le tuto mais d'après les commentaires il parait être excellent, cependant et comme je suis nouveau je me demandais comment pouvoir le télécharger ?  

Merci à vous.

Commentaire de Bacterius le 22/04/2009 15:03:58

Ben tu peux enregistrer la page web ?
Fichier => Enregistrer sous ...
Ca te donne le fichier .html + un dossier contenant les images.
Ou alors, tu fais un copier-coller de tout le texte, et de l'image, dans OpenOffice ou Word ça devrait marcher ;)

Cordialement, Bacterius !

Commentaire de Bacterius le 22/04/2009 18:50:08

Bon je fais une MAJ de ce tutorial ce soir, avec :
- illustration de scanline
- remarque de Cantador (opérations longues avec variables, courtes avec pointeurs).

Cordialement, Bacterius !

Commentaire de Bacterius le 22/04/2009 20:23:40

Bon alors j'ai mis la MAJ pour Scanline, mais je n'avais pas d'idée pour la remarque de Cantador, je m'en occuperai plus tard.
Par contre la textbox a litteralement bousillé mes schémas pour Scanline ... j'espère que vous comprendrez quand-même ...

Cordialement, Bacterius !

Commentaire de swatche001 le 23/04/2009 14:42:56

bien fait bonne continuation

Commentaire de Bacterius le 23/04/2009 14:44:21

Merci Swatch :)

Cordialement, Bacterius !

Commentaire de amiga68 le 24/06/2009 07:29:21

Merci maître ! Très clair et très utile.

j'ai malheureusement relevé un ch''ite erreur (d'inattention, mais votre cerveau étant tellement occupé...) en-dessous de la note qui suit le petit schéma de la partie III

==> Ici, le pointeur est de type mot double, le compilateur sait que la variable est donc de type mot double, et que donc la longueur de la variable est de 4 octets. Notez que Delphi autorise que certaines variables comment à la même adresse mémoire que d'autres

ne serait-ce point :
==> Notez que Delphi autorise que certaines variables commenCEnt à la même adresse

Commentaire de Bacterius le 24/06/2009 09:32:46

Ouhla effectivement, quelque chose à echappé à mon oeil perçant. Je corrige ça dans la journée. Merci Amiga68 :)

Cordialement, Bacterius !

Commentaire de chperetti le 11/03/2010 15:37:02

Bonjour

Merci pour ce très bon tutoriel

malheureusement je ne trouve pas une réponse à mon problème

j'ai un composant qui dans une procedure me donne :

procedure TForm1.S_DataAvailable(Sender TObject;Buffer : Pointer;Var Len : Integer);

Quelle commande utiliser pour transformer "BUFFER" qui est un pointeur dans un string (Var S : String);

Merci d'avance et meilleures Salutations

Christophe




Commentaire de Bacterius le 11/03/2010 19:45:08

Salut,
Tu peux convertir un string en pointeur en utilisant le transtypage suivant :

PAnsiChar(S) pour le pointeur
PAnsiChar(S)^ pour le buffer (non typé) correspondant au string

J'utilise souvent cette astuce.

Cordialement, Bacterius !

Commentaire de jihelb le 30/04/2010 12:32:43

BRAVO !!!
JiHelB

Commentaire de shaoul0 le 29/07/2010 11:27:23

tuto intéressant

j'ai une question concernant les tableaux multidimensionnels
ex matrice[1..3, 1..4, 1..5] de type integer (ou autre)
on aura donc 3 * 4 * 5 = 60 éléments
mais dans quel ordre ?

matrice [1,1,1]
matrice [1,1,2]
matrice [1,1,3]
matrice [1,1,4]
matrice [1,1,5]
matrice [1,2,1]
matrice [1,2,2]
...
matrice [3,4,5]

ou

matrice [1,1,1]
matrice [2,1,1]
matrice [3,1,1]
matrice [1,2,1]
matrice [2,2,1]
...
matrice [3,4,5]

ou bien cela dépendra du compilateur, de la machine cible, de l'air du temps, ...
ou bien ce type de variables est de toute façon passée par pointeur





Commentaire de Caribensila le 02/08/2010 11:31:41

Re-salut,

A propos de ScanLine, tu écris :
« D'abord, n'oublions pas qu'un bitmap est stocké à l'envers dans un fichier (la ligne 1 passe en dernier, la ligne 2 en avant-dernier ...). Ce qui fait que l'ordre des pixels est différent. »

C'est souvent le cas, mais ce n'est pas toujours vrai ( ce serait trop beau :)
Il existe en effet 2 sortes de BIB (Device Independant Bitmap) :

- le bottom-up DIB. Tels ceux produits par le GDI et dont tu parles. Ils sont stockés en mémoire de cette façon:
Ligne[H-1]
Ligne[H-2]
...
Ligne[1]
Ligne[0]

- le top-down DIB. Tels ceux avec lesquels travaille DirectDraw, par exemple. Ils sont stockés en mémoire de cette façon:
Ligne[0]
Ligne[1]
...
Ligne[H-2]
Ligne[H-1]

Pourquoi ?   - C'est un héritage des premiers temps de l'info où l'on s'est aperçu qu'une mémorisation à l'envers (bottom-up) permettait un affichage plus rapide du Bitmap; et c'est resté par souci de compatibilité et pour faire parler les programmeurs.

Comment les reconnaître ?  - Il faut consulter le membre biHeight de la structure BITMAPINFOHEADER :
biHeight positif =>  C'est un bottom-up DIB
biHeight négatif =>  C'est un top-down DIB

Voilà, je tenais à le préciser car ça peut expliquer certains bugs parmi les plus chiants, les bugs "un coup ça marche, un coup ça marche pas".  ;)

Commentaire de Caribensila le 02/08/2010 11:53:32

... Et comment consulter le membre biHeight de la structure BITMAPINFOHEADER ?   :)

type
TDIBinfos = record
         BitmapInfo      : TBitmapInfo; // Renvoie une structure BITMAPINFO.
         BitsPerPixel    : Cardinal;   // Nombre de bits par pixel.
         BottomUpDIB     : Boolean;   // bottom-up DIB ou top-down DIB.
         MemLineByteSize : Cardinal; // Longeur d'une ligne de pixels en mémoire en octets.
         MemImageSize    : Cardinal;// Taille du tableau de pixels en mémoire en octets.
    end;

function GetDIBInfos(BMP : TBitmap): TDIBinfos;
var
         BmpSection      : DIBSECTION;
         pImage          : Pointer;
begin
  GetObject(BMP.Handle, SizeOf(DIBSECTION), @BmpSection);
with BmpSection.dsBm, BmpSection.dsBmih, Result do begin
   BitsPerPixel         := biBitCount; //ou bmBitsPixel.
   MemLineByteSize      := bmWidthBytes;
    MemImageSize         := biSizeImage;
   if biHeight<0 then BottomUpDIB := false else BottomUpDIB := true;
   GetMem(pImage, MemImageSize);
   try
   GetDIB(BMP.Handle, 0, BitmapInfo, pImage^); //On récupère la structure BITMAPINFO pour GetDIBits et SetDIBits.
   finally FreeMem(pImage, MemImageSize);   end;
  end;
end;

Commentaire de karthos231 le 07/11/2010 11:23:14

Non j'adore ton tutoriel Merci

Commentaire de hamadit le 04/12/2010 22:22:16

BIEN

Commentaire de Reality31600 le 18/03/2011 23:12:24

Salut,

Tutoriel très bien expliquer et vraiment sympa à lire.
Merci pour ce partage de connaissance !

Bonne continuation !

 Ajouter un commentaire




Nos sponsors


Sondage...

CalendriCode

Février 2012
LMMJVSD
  12345
6789101112
13141516171819
20212223242526
272829    

Consulter la suite du CalendriCode

 
Développement réalisé par Nicolas SOREL (Nix) avec l'aide de : Cyril DURAND et Emmanuel (EBArtSoft), Merci à Vincent pour ses précieux conseils.
CodeS-SourceS.com© Toute reproduction même partielle est interdite sauf accord écrit du Webmaster
CodeS-SourceS.com© est une marque déposée tous droits réservés

Google Coop CodeS-SourceS Google Coop CodeS-SourceS
Temps d'éxécution de la page : 0,078 sec (4)

Nous contacter | Annoncer sur CodeS-SourceS | Mentions légales