Trois types de données à échanger entre processus

Jean-Luc Mellet

L'échange de données entre processus (applications) peut être réalisé de bien des façons. On peut utiliser les messages Windows (à condition que les processus soient exécutés sur le même système), les mailslots, les sockets (nécessitent TCP/IP), le mappage de fichier, etc.

J'ai voulu décrire ici l'utilisation des messages Windows pour échanger des données entre processus. La simplicité des trois méthodes décrites vous convaincra que tout cela est finalement très facile à réaliser.

À partir d'un exemple, nous allons voir l'échange de valeur numérique entière, de chaîne de caractères et de structure (record).

Note : ce document sera émaillé de nombreux morceaux de code, mais vous trouverez un lien en fin de document pour charger les deux projets exemples au format D5 et au format texte pour ceux qui auraient une version antérieure.

- Titre : Trois types de données à échanger entre processus

- Auteur : Jean-Luc Mellet

- Parution : 14 février 2003

- Mise à jour : 25 décembre 2019

- Licence : Copyright ® 2003 Jean-Luc Mellet. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Les messages sous Windows

- II. Envoi d'un nombre

- III. Envoi d'une chaîne de caractères

- IV. Envoi d'une structure

- V. Conclusion

I - Les messages sous Windows

Rappelons brièvement que Windows est un système basé sur le déclenchement d'événements. Il y a un échange permanent de messages divers entre le système d'exploitation et les processus actifs (applications en cours d'exécution). La plus grande majorité de ces messages est déclenchée par une intervention utilisateur (mouvement souris, entrée clavier, etc.). Chaque processus est « à l'écoute » des messages qui peuvent lui parvenir. Cela s'appelle la boucle de message (Message Loop). Plusieurs messages pouvant arriver successivement et rapidement, il existe ce que l'on appelle une « queue de messages » (Message Queue). Vous aurez compris que chaque message est traité dans l'ordre d'arrivée, du plus ancien au plus récent.

Comment cela fonctionne-t-il ? Sans entrer dans des détails complexes, disons simplement qu'une application Windows traditionnelle possède une fonction winmain (le cœur) dans laquelle est codée cette fameuse boucle de message, qui continue à tourner tant que des messages sont censés lui parvenir. Chaque message reçu est traité selon sa fonction prédéfinie. Imaginez un entrepôt de marchandises diverses avec une personne à la réception. Toute marchandise arrivant à la porte de l’entrepôt est examinée et dirigée vers son lieu de stockage ou de fonction. Cette boucle réalise exactement la même chose.

Alors, si une application reçoit des messages du système (et des autres processus), nous allons pouvoir faire communiquer deux processus entre eux, justement par l'envoi de messages. Dans le système, il existe une grande quantité de messages prédéfinis par des constantes. La plus connue de ces constantes se nomme WM_USER et sa valeur est de $0400 (1024 en décimal). Windows se réservant les 1024 premières valeurs pour ses messages prédéfinis, il nous reste toutes les valeurs supérieures à 1024 (WM_USER + n) pour définir nos propres constantes. En fait, pas tout à fait… On pourrait se dire :

« Puisque Windows garde les 1024 premières valeurs, je vais utiliser la constante WM_MON_MESSAGE_A_MOI avec la valeur 1025 ! »

Ce serait une erreur ! Pourquoi ? Et bien, parce que les fonctions SendMessage, PostMessage et autres peuvent envoyer un message à toutes les applications en cours d'exécution. C'est ce qu'on appelle le broadcast. Imaginez qu'un programmeur ait déjà utilisé cette valeur pour une application et que vous exécutiez cette application en même temps que la vôtre ! Quand vous utiliserez le message WM_MON_MESSAGE_A_MOI, votre programme le recevra, mais l'autre programme le pourrait aussi. Si la réception du message numéro 1025 par l'autre programme demande la fermeture de Windows, vous imaginez le résultat ?

Nous allons utiliser une API qui va nous fournir un numéro de message unique. Il s'agit de RegisterWindowMessage. Comment l'utiliser ? Il suffit de lui passer en paramètre une chaîne de caractères pour qu'elle nous fournisse une valeur unique pour le système. Une fois cette valeur définie, elle sera utilisable jusqu'à l’arrêt du système. Par exemple, pour obtenir une valeur unique pour WM_MON_MESSAGE_A_MOI, nous pourrions écrire :

WM_MON_MESSAGE_A_MOI := RegisterWindowMessage('WM_MON_MESSAGE_A_MOI');

J'ai repris le nom de la variable sous forme de chaîne, mais cela n'est pas une obligation. Vous pourriez tout aussi bien écrire :

WM_MON_MESSAGE_A_MOI := RegisterWindowMessage('Je veux un numéro de message unique');

Cela fonctionnera tout aussi bien.

L'envoi des messages ainsi créés se fait principalement par l'intermédiaire des deux API SendMessage et PostMessage. Quelle différence entre les deux ? Souvenez-vous de notre fameuse boucle de message ! Je vous disais que le traitement se faisait de façon séquentielle. Et bien SendMessage envoie le message, l'ajoute à la queue de messages, attend que le message soit transmis et ensuite seulement renvoie un résultat. PostMessage envoie le message et renvoie immédiatement un résultat sans attendre la transmission du message (son passage dans la boucle d'attente).

L'envoi de message étant expliqué, il nous reste à voir comment recevoir un message bien précis. Pour cela, deux solutions ! Soit on crée une procédure qui ne recevra QUE le message désiré, soit on surcharge la procédure qui s'occupe de tous les messages non traités par la fiche, et on filtre les messages selon leur fonction. Il s'agit de la procédure DefaultHandler que l'on peut surcharger. Que dit l'aide Delphi à ce propos ?

DefaultHandler est une procédure de TCustomForm qui gère tous les messages que la fiche n'a pas entièrement traités. procedure DefaultHandler(var Message); override;

Description

Surchargez la méthode DefaultHandler pour modifier le traitement par défaut des messages de la fiche. Ceci est rarement nécessaire, car les messages peuvent être gérés en créant des méthodes de message.

DefaultHandler transmet tous les messages non gérés à la procédure de fenêtre de la fiche en appelant la fonction CallWindowProc de l'API.

Remarque : l'appel de la méthode héritée dans une méthode de gestion des messages produit l'appel de la méthode DefaultHandler de l'ancêtre si celui-ci ne spécifie pas de gestionnaire pour le message à gérer.

Pour notre exemple, elle nous sera nécessaire à cause de l'utilisation de RegisterWindowMessage. La création d'une procédure pour gérer un message unique est simple. La déclaration doit se faire dans la section public de la TForm. Par exemple, pour notre valeur précédente, on écrira :

1. const

2. WM_MON_MESSAGE_A_MOI : integer = WM_USER + 1;

4. procedure WmMonMessageAmoi(var M: TMessage); message WM_MON_MESSAGE_A_MOI;

6. implementation

8. procedure WmMonMessageAmoi(var M: TMessage);

9. begin

10. // Ici, on ne traite QUE le message WM_MON_MESSAGE_A_MOI;

11. end;

Vous avez remarqué que WM_MON_MESSAGE_A_MOI doit être une constante. À cause de cela, nous utiliserons la procédure DefaultHandler plutôt qu'une gestion de message au coup par coup. Pourquoi ? Tout simplement parce que la valeur de WM_MON_MESSAGE_A_MOI ne sera connue qu'au lancement du programme par l'utilisation de RegisterWindowMessage. Il est donc impossible de définir sa valeur lors de la conception du code.

Tout au long de ce document, j'utiliserai SendMessage. Alors, voyons comment utiliser cette API pour un simple échange de données entre applications ! Pour illustrer ce travail, nous allons créer deux projets, l'un sera l'émetteur et l'autre le receveur.

Dans nos deux projets, nous allons définir (pour plus de clarté) les mêmes noms pour nos variables identificateur de message. Vous noterez que l'utilisation de la même chaîne de caractères renverra la même valeur numérique à nos deux programmes, nous permettant de connaître ces valeurs sans autre recherche.

IMPORTANT : (pour trouver le handle de chaque application)

La fiche de l'application Emetteur aura comme titre : « Cette appli envoie les infos au receveur ».

La fiche de l'application Receveur aura comme titre : « Cette appli reçoit les infos ».

Code émetteur ET receveur (similaires pour l'instant)

1. implementation

3. {$R *.DFM}

5. var

6. WM_MESSAGE_ENVOYEUR, WM_ENVOI_ATOM: integer;

8. procedure TForm1.FormCreate(Sender: TObject);

9. begin

10. WM_MESSAGE_ENVOYEUR := RegisterWindowMessage('WM_MESSAGE_ENVOYEUR');

11. WM_ENVOI_ATOM := RegisterWindowMessage('WM_ENVOI_ATOM');

12. end;

Dans notre receveur, nous allons surcharger DefaultHandler.

Code Receveur

1. public

2. procedure DefaultHandler(var msg); override;

4. begin

5. inherited DefaultHandler(Msg);

6. end;

Voilà ! À ce stade, nos deux applications peuvent dialoguer. Ou plus exactement, notre Receveur peut récupérer des messages, y compris ceux de notre émetteur.

II - Envoi d'un nombre

Le cas le plus simple de besoin d'échange de données est l'échange d'une valeur entière. Les paramètres de SendMessage sont respectivement la handle de l'application qui devra recevoir le message, le numéro du message, et deux valeurs entières.

Sur la fiche de notre Emetteur, nous allons placer un TEdit et un TButton. Bridons notre TEdit pour imposer la saisie de valeur entière uniquement en modifiant l'événement OnKeyPress, et modifions l'événement OnClick du TButton pour envoyer notre valeur numérique. Pour envoyer notre message, nous devons connaître le handle du programme Receveur. Nous le trouverons en utilisant l'API FindWindow.

Code émetteur

1. procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);

2. var

3. S: string;

4. err, valeur: integer;

5. begin

6. if Key in ['0'..'9', #8] then

7. begin

8. S := Edit2.text + Key;

9. val(S, Valeur, err);

10. if err <> 0 then Key := #0;

11. end

12. else

13. Key := #0;

14. end;

15. end;

17. procedure TForm1.Envoyer1Click(Sender: TObject);

18. var

19. h: THandle;

20. begin

21. {Envoie au receveur le nombre indiqué dans Edit1.text}

22. if Edit1.text = '' then

23. ShowMessage('Veuillez entrer un nombre ')

24. else

25. begin

26. h := FindWindow(nil, PChar('Cette appli reçoit les infos'));

27. if h = 0 then

28. ShowMessage('Le receveur est inactif')

29. else

30. SendMessage(h, WM_MESSAGE_ENVOYEUR, StrToInt(Edit1.text), 0);

31. end;

32. end;

Que va-t-il se passer dans notre Receveur ? Pour mieux le montrer, rajoutons à notre fiche Receveur une un TLabel pour y inscrire le résultat.

Code receveur

1. procedure TForm1.DefaultHandler(var msg);

2. begin

3. inherited DefaultHandler(Msg);

4. if TMessage(msg).Msg = WM_MESSAGE_ENVOYEUR then Label1.Caption := IntToStr(TMessage(msg).WParam);

5. end;

Si DefaultHandler reçoit le message WM_MESSAGE_ENVOYEUR, il affiche la valeur de WParam (valeur passée par le message).

III - Envoi d'une chaîne de caractères

Pour illustrer l'envoi d'une chaîne de caractères, nous allons parler des atom. Aucune radiation ni reste de Tchernobyl là-dedans, rassurez-vous ! Un atom n'est rien qu'une string[255 {maxi}] avec un identifiant numérique stocké soit au niveau du processus, soit au niveau global du système. Chaque atom peut donc être retrouvé avec son identificateur numérique. La table est globale et peut être partagée par tous.

Pour transmettre une chaîne, il va donc nous suffire d'ajouter un atom global au système et d'envoyer son identifiant numérique au Receveur qui, à partir de ça, pourra retrouver la chaîne dans la table. Pour l'exemple, utilisons un TEdit pour la saisie du texte et un TButton pour l'envoi du message. Pour ajouter un atom à la table globale, nous utiliserons GlobalAddAtom.

Code émetteur

1. procedure TForm1.Envoyer2Click(Sender: TObject);

2. var

3. h: THandle;

4. atom_Envoye: Atom;

5. begin

6. if length(edit2.text) = 0 then

7. ShowMessage('Vous devez saisir un texte à envoyer')

8. else

9. {Création d'un atom dans la table globale

10. Notification par message au receveur de l'envoi d'un atom}

11. begin

12. atom_Envoye := GlobalAddAtom(PChar(edit2.text));

13. h := FindWindow(nil, PChar('Cette appli reçoit les infos'));

14. if h = 0 then

15. ShowMessage('Le receveur est inactif')

16. else

17. SendMessage(h, WM_ENVOI_ATOM, atom_Envoye, 0); {l'atom est envoyé dans WParam}

18. end;

19. end;

Que va-t-il se passer dans notre receveur ? Et bien nous allons filtrer le nouveau message possible et agir en conséquence.

Code receveur

1. procedure TForm1.DefaultHandler(var msg);

2. begin

3. inherited DefaultHandler(Msg);

5. if TMessage(msg).Msg = WM_MESSAGE_ENVOYEUR  then

6. Label1.Caption := IntToStr(TMessage(msg).WParam)

8. else if TMessage(msg).Msg = WM_ENVOI_ATOM  then

9. begin

10. // le numéro identifiant l'atom se trouve dans WParam

11. atom_recu := TMessage(msg).WParam;

12. GetMem(TexteRecu, 256);   {255 maxi + #0}

13. GlobalGetAtomName(atom_recu, TexteRecu, 256);

14. label5.caption := TexteRecu;

15. GlobalDeleteAtom(atom_recu);   {Ne pas oublier de détruire l'atom puisqu'on a récupéré la valeur}

16. FreeMem(TexteRecu);

17. end;

19. end;

IV - Envoi d'une structure

Nous allons terminer cette mini revue de l'utilisation des messages avec celui qui permet de transmettre une structure, et donc, tout et n'importe quoi. Le message utilisé sera WM_COPYDATA. Puisque ce message fait partie de ceux prédéfinis par Windows, nous allons pouvoir créer dans notre Receveur une procédure qui s'occupera principalement de lui. Pour l'exemple, j'ai ajouté sur l'Emetteur et le Receveur les éléments suivants :

L'émetteur transmettra sous forme de structure les différentes infos de ce TPanel. Là, le traitement est un peu plus compliqué. Les paramètres sont : handle de l'application Receveur, le message WM_COPYDATA, le handle de l'application émettrice, un pointeur sur une structure de type TCopyDataStruct.

1. TCopyDataStruct = record

2. dwData: LongInt;      {une valeur au choix de l'utilisateur}

3. cbData: LongInt;       {taille de la structure de données envoyée}

4. lpData: Pointer;         {pointeur sur la structure}

5. end;

Code émetteur ET receveur (parties communes)

1. implémentation

3. type

4. PCopyDataStruct = ^TCopyDataStruct;

5. TCopyDataStruct = record

6. dwData: LongInt;

7. cbData: LongInt;

8. lpData: Pointer;

9. end;

11. type

12. PEnvoiFax = ^TEnvoiFax;

13. TEnvoiFax = packed record

14. Destinataire: string[255];

15. TypeDocument: integer;

16. DateEnvoi: TDateTime;

17. EnvoiFait: boolean;

18. end;

Code émetteur

1. procedure TForm1.Envoyer3Click(Sender: TObject);

2. var

3. h: THandle;

4. CopyDataStruct: TCopyDataStruct;

5. Envoi: TEnvoiFax;

6. begin

7. {Envoyer une structure de données avec WM_COPYDATA}

8. h := FindWindow(nil, PChar('Cette appli reçoit les infos'));

9. if h = 0 then

10. ShowMessage('Le receveur est inactif')

11. else

12. begin

13. with Envoi do

14. begin

15. Destinataire := Edit1.text;

16. TypeDocument := RadioGroup1.ItemIndex;

17. DateEnvoi    := DateTimePicker1.DateTime;

18. EnvoiFait    := CheckBox1.Checked;

19. end;

20. with CopyDataStruct do

21. begin

22. dwData := 1;

23. cbData := sizeof(Envoi);

24. lpData := @Envoi;

25. end;

26. SendMessage( (h, WM_COPYDATA, Form1.Handle, LongInt(@CopyDataStruct));

27. end;

28. end;

Code receveur

1. public

2. procedure WmCopyData(var M: TMessage); message WM_COPYDATA;

4. implémentation

6. procedure TForm1.WmCopyData(var M: TMessage);

7. begin

8. with PEnvoiFax(PCopyDataStruct(m.LParam)^.lpData)^ do

9. begin

10. Edit1.text := Destinataire;

11. RadioGroup1.ItemIndex := TypeDocument;

12. DateTimePicker1.DateTime := DateEnvoi;

13. CheckBox1.Checked := EnvoiFait;

14. end;

15. end;

V - Conclusion

Comme vous le voyez, l'échange de données est une chose simple. Comme je le précisais au début de cet exposé, il existe bien d'autres méthodes, mais les trois décrites ici suffisent le plus souvent à régler tous les problèmes d'échange.

À bientôt pour une autre aventure !

Source D5

Passez donc me voir !