Tutorial 21: Le Pipe
Pipe = Cable de communication entre deux programmes

(en plus : couleur du fond d'un contrôle d'édition)

Dans ce Tutorial, nous allons nous intéresser au 'Pipe', ce que ça représente et à quoi nous pouvons l'employer. Pour rendre tout ça plus intéressant, j'ajoute en plus à cette technique le fait de savoir comment changer le fond et la couleur du texte d'un contrôle d'édition.
Downloadez l'exemple ici.
En réalité cet exemple fait appel au programme ml.exe qui se trouve dans C:\Masm32\Bin\ donc vous devez d'abord le dézipper dans ce dossier pour qu'il puisse fonctionner.

Théorie:

Le pipe est un conduit ou bien une sorte de cable de communication muni de deux bouts. Vous pouvez l'employer pour échanger des données entre deux processus différents (entre deux programmes différents), ou dans un même processus. C'est comme un walkie-talkie. Vous donnez une information à l'autre partie et elle peut l'employer pour communiquer avec vous.
Il y a deux types de pipes : les pipes anonymes et les pipes nommées. Un pipe anonyme est, eh bien, anonyme : c'est-à-dire que vous pouvez l'utiliser sans connaître son nom. Pour un pipe nommée c'est le contraire : vous devez connaître son nom avant de pouvoir l'utiliser.
Vous pouvez aussi ranger les pipes par catégories selon leur propriété : certains fonctionnent en sens unique, les autres communique bilatéralement. Dans un pipe à sens unique, les données circulent uniquement dans une seule direction : d'un bout à l'autre. Tandis que pour un pipe bilatérale, les données peuvent être échangées entre les deux bouts.
Un pipe anonyme est toujours à sens unique tandis qu'un pipe nommée peut être à sens unique ou bilatérale. Un pipe nommée est d'habitude employé dans un environnement réseau dans lequel un serveur peut se connecter à plusieurs clients.
Dans ce Tutorial, nous examinerons le pipe anonyme en détail. Le but principal d'un pipe anonyme c'est d'être utilisé comme un cable de communcation entre un parent et des processus enfants (entre un programme principal et ses sous-programmes) ou entre plusieurs processus enfants.
Un pipe anonyme est réellement utile lorsque vous traitez avec une application console. Une application console est une sorte de programme win32 qui utilise une console en tant qu'Entrée/Sortie. Une console est un outils de DOS. Cependant, une application de console est un programme entièrement 32 bits. Elle peut employer n'importe quelle fonction du GUI (NdT : Graphic User Interface, le GUI c'est l'ensemble des objets tels qu'une boîte de saisie de texte, les boutons…), de la même façon que d'autres programmes.
Une application console a trois handles qu'elle peut utiliser pour ses Entrées/Sorties. Ont les appelle 'handles standards'. Il y en a trois : 'Entrée Standard'(ou Standard Input), 'Sortie Standard'(ou Standard Output) et 'Erreur Standard'(ou Standard Error). L'handle de l'Entrée Standard est employé pour lire/récupérer les informations de la console, alors que l'handle de la Sortie Standard est utilisé pour renvoyer les données vers la sortie/ou l 'imprimante. L'handle de l'Erreur Standard est employé pour donner un compte rendu sur les conditions d'erreurs lorsque sa sortie n'est pas accessible.
Une application console peut retrouver ces trois 'handles standards' en appelant la fonction GetStdHandle, en spécifiant bien quel handle est ce qu'on souhaite récupérer. Une application GUI n'a pas de console. Si vous appelez GetStdHandle, il renverra une erreur. Si vous voulez vraiment utiliser une console, vous pouvez appeler AllocConsole pour allouer une nouvelle console. Cependant, n'oubliez pas d'appeler FreeConsole quand vous mettez en place une console.
Le pipe anonyme est fréquemment employée pour faire suivre les entrées/sorties d'une console d'une application enfant. Le processus parent peut être une console ou une application du GUI, mais l'enfant doit être un appendice de console. Pour que ça marche. Comme vous le savez, une application console utilise des handles standards pour ses Entrées/Sorties. Si nous voulons faire suivre les Entrées/Sorties d'une application console, nous pouvons remplacer l'handle par un des handles du pipe. Une application console ne se rendra pas compte qu'on emploie l'handle d'un pipe. Elle l'utilisera de la même manière qu'un handle normal. C'est une sorte de polymorphisme, en le jargon OOP. Cette approche est puissante puisque nous n'avons pas besoin de modifier le process enfant où que ce soit.
Une autre chose que vous devez savoir à propos des applications console c'est à quel moment on récupère ces 'handles standards'. Lorsqu'une application console est créée, le parent process a deux choix : il peut créer une nouvelle console pour l'enfant ou il peut laisser l'enfant hériter de sa propre console (le parent process et le child prosses auront des consoles de mêmes caractéristiques). Avec la deuxième approche, le parent process doit être une application console ou bien si c'est une application du GUI, elle doit d'abord appeler AllocConsole pour allouer une console.
On va commencer le boulot. Pour créer un pipe anonyme vous avez besoin d'appeler CreatePipe. CreatePipe a le prototype suivant :
CreatePipe proto pReadHandle:DWORD, \
       pWriteHandle:DWORD,\
       pPipeAttributes:DWORD,\
       nBufferSize:DWORD
Si l'appel est couronné de succès, la valeur en retour est différent de zéro. Si elle a échouée, la valeur de retour est nulle.
Après que l'appel est été couronné de succès, vous obtiendrez deux handles, un pour le bout 'lecture' du pipe, et autre pour le bout 'écriture'. Maintenant on va voir les étapes nécessaires à la réexpédition des Sorties Standards d'un programme console enfant, vers votre propre process. Notez que ma méthode diffère de celle de la référence win32 api de Borland. La méthode dans la référence win32 api impose que le parent process soit une application console et par conséquent le prosses enfant peut hériter de ses 'handles standards'. Mais la plupart du temps, nous aurons besoin de faire suivre la Sortie d'une application console vers une application du GUI (par exemple vers une boîte de saisie).
  1. Créez une pipe anonyme avec CreatePipe. N'oubliez pas de mettre le membre bInheritable à TRUE dans la structure SECURITY_ATTRIBUTES puisque les handles ne proviennent pas d'un parent.
  2. Maintenant nous devons préparer les paramètres que nous passerons à CreateProcess puisque nous l'emploierons pour charger l'application console enfant. Une structure importante est la structure de STARTUPINFO. Elle détermine l'apparence de la fenêtre principale du process enfant quand elle apparaît en premier lieu. Cette structure est essentielle pour nous. Vous pouvez cacher la fenêtre principale et passer l'handle du pipe au process console enfant comme ça. Ci-dessous voici les membres que vous devez remplir :
  3. On appelle CreateProcess pour charger l'application enfant. Après que CreateProcess soit couronné de succès, l'enfant est toujours inactivé. Il est chargé en mémoire mais il n'est pas immédiatement en action.
  4. On ferme l'handle d'écriture du pipe. C'est nécessaire. Parce que le parent process n'a pas besoin de se servir de l'handle d'écriture du pipe. En plus, le pipe ne fonctionnera pas si il y a plus d'une sortie d'écriture, nous DEVONS le fermer avant la lecture des données du pipe. Cependant, ne fermez pas l'handle d'écriture avant l'appel CreateProcess, sinon le fonctionnement de votre pipe sera transgressé. Vous devez seulement le fermer après le retour de CreateProcess mais surtout pas avant que vous n'ayez lu les données de lecture du pipe.
  5. Maintenant vous pouvez lire les données du bout de lecture du pipe avec ReadFile. Grâce à ReadFile, vous influencez le process enfant pendant son déroulement même. Ça débute l'exécution et lorsqu'on ''écrit'' quelque chose à l'handle standard de sortie (qui est en réalité l'handle d'écriture du pipe), les données sont envoyées par le pipe vers le bout d'écriture. Vous pouvez vous représenter ReadFile un peu comme un aspirateur de données du bout d'écriture du pipe. Vous devez appeler ReadFile à plusieurs reprises avant qu'il ne retourne la valeur 0, ce qui signifie qu'il n'y a plus de données à lire. Vous pouvez faire ce qu'il vous plais des données lues dans le pipe. Dans notre exemple, je les ai mises dans un contrôle d'édition. (cas classique)
  6. On ferme l'handle de lecture du pipe.

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101         ; l' ID du menu principal
IDM_ASSEMBLE equ 40001

.data
ClassName            db "PipeWinClass",0
AppName              db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError     db "Error during pipe creation",0
CreateProcessError     db "Error during process creation",0
CommandLine     db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
    invoke GetModuleHandle, NULL
    mov hInstance,eax
    invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
    invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
    LOCAL wc:WNDCLASSEX
    LOCAL msg:MSG
    LOCAL hwnd:HWND
    mov wc.cbSize,SIZEOF WNDCLASSEX
    mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
    mov wc.cbClsExtra,NULL
    mov wc.cbWndExtra,NULL
    push hInst
    pop wc.hInstance
    mov wc.hbrBackground,COLOR_APPWORKSPACE
    mov wc.lpszMenuName,IDR_MAINMENU
    mov wc.lpszClassName,OFFSET ClassName
    invoke LoadIcon,NULL,IDI_APPLICATION
    mov wc.hIcon,eax
    mov wc.hIconSm,eax
    invoke LoadCursor,NULL,IDC_ARROW
    mov wc.hCursor,eax
    invoke RegisterClassEx, addr wc
    invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\ CW_USEDEFAULT,400,200,NULL,NULL,\ hInst,NULL
    mov hwnd,eax
    .while TRUE
        invoke GetMessage, ADDR msg,NULL,0,0
        .BREAK .IF (!eax)
        invoke TranslateMessage, ADDR msg
        invoke DispatchMessage, ADDR msg
    .endw
    mov eax,msg.wParam
    ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
    LOCAL rect:RECT
    LOCAL hRead:DWORD
    LOCAL hWrite:DWORD
    LOCAL startupinfo:STARTUPINFO
    LOCAL pinfo:PROCESS_INFORMATION
    LOCAL buffer[1024]:byte
    LOCAL bytesRead:DWORD
    LOCAL hdc:DWORD
    LOCAL sat:SECURITY_ATTRIBUTES
    .if uMsg==WM_CREATE
        invoke CreateWindowEx,NULL,addr EditClass, NULL, WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL, 0, 0, 0, 0, hWnd, NULL, hInstance, NULL
        mov hwndEdit,eax
    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetBkColor,wParam,Black
       invoke GetStockObject,BLACK_BRUSH
        ret
    .elseif uMsg==WM_SIZE
        mov edx,lParam
        mov ecx,edx
        shr ecx,16
        and edx,0ffffh
        invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
    .elseif uMsg==WM_COMMAND
       .if lParam==0
            mov eax,wParam
            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE
                invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
                .if eax==NULL
                    invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
                .else
                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE
                    invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo
                    .if eax==NULL
                        invoke MessageBox,hWnd,addr CreateProcessError,addr         AppName,MB_ICONERROR+MB_OK
                    .else
                        invoke CloseHandle,hWrite
                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw
                    .endif
                    invoke CloseHandle,hRead
                .endif
            .endif
        .endif
    .elseif uMsg==WM_DESTROY
        invoke PostQuitMessage,NULL
    .else
        invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
    .endif
    xor eax,eax
    ret
WndProc endp
end start

Analyse:

Notre exemple appellera le petit programme ml.exe pour assembler un fichier nommé test.asm, puis fera suivre la sortie (les données en sortie) de ml.exe vers le contrôle d'édition (du secteur client de notre programme).
Quand le programme est chargé, on enregistre la 'window class' et on crée comme d'habitude la fenêtre principale. La première chose qu'on fait pendant la création de la fenêtre principale c'est de créer un contrôle d'édition qui sera employé pour afficher les données de sortie de ml.exe (pour afficher par exemple: ml.exe vient d'assembler Test.asm avec succès…..).
Maintenant nous voici dans la partie intéressante (mais c'est le deuxième point de ce tutorial. Ça n'a aucun rapport avec ce qu'on a vu avant), nous allons changer la couleur du texte et la couleur de fond du contrôle d'édition. Quand un contrôle d'édition repeindre son secteur client, il envoie le message WM_CTLCOLOREDIT à sa fenêtre parente..
wParam contient l'handle du style d'écriture que le contrôle d'édition emploiera pour écrire son propre secteur de client. Nous pouvons profiter de cette occasion pour modifier les caractéristiques de l'HDC.
    .elseif uMsg==WM_CTLCOLOREDIT
        invoke SetTextColor,wParam,Yellow
        invoke SetTextColor,wParam,Black
        invoke GetStockObject,BLACK_BRUSH
        ret
SetTextColor change la couleur du texte en jaune. SetTextColor change la couleur du fond en noir. Et finalement, nous obtenons l'handle du pinceau noir que nous renvoyons à Windows. Avec le message WM_CTLCOLOREDIT, on renvoie l'handle du pinceau que Windows emploiera pour repeindre le fond du contrôle d'édition. Dans notre exemple, je souhaite que le fond soit noir, donc je renvoie à Windows l'handle du pinceau noir.
Maintenant quand l'utilisateur choisit le menuitem (le sous-menu) Assemble, il crée un pipe anonyme.

            .if ax==IDM_ASSEMBLE
                mov sat.niLength,sizeof SECURITY_ATTRIBUTES
                mov sat.lpSecurityDescriptor,NULL
                mov sat.bInheritHandle,TRUE

Avant l'appel CreatePipe, nous devons d'abord remplir la structure SECURITY_ATTRIBUTES. Notez que nous pouvons employer NULL dans le membre lpSecurityDescriptor si nous ne nous soucions pas de la sécurité. Et le membre bInheritHandle doit être TRUE pour que les handles du pipe soient les mêmes pour le process parent et le process enfant.

               invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL

Après ça, nous appelons CreatePipe qui, si il est couronné de succès, remplira les variables hRead et hWrite respectivement avec les handles de lecture et d'écriture des bouts du pipe.

                    mov startupinfo.cb,sizeof STARTUPINFO
                    invoke GetStartupInfo,addr startupinfo
                    mov eax, hWrite
                    mov startupinfo.hStdOutput,eax
                    mov startupinfo.hStdError,eax
                    mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES
                    mov startupinfo.wShowWindow,SW_HIDE

Ensuite nous devons remplir la structure STARTUPINFO. Nous appelons GetStartupInfo pour remplir la structure STARTUPINFO avec les valeurs de défaut du process parent. Vous DEVEZ remplir la structure STARTUPINFO avec cet appel si vous avez l'intention que votre code puisse travailler à la fois sous win9x et NT. Après les valeurs retournées par l'appel GetStartupInfo, vous pouvez modifier les membres qui sont importants. Nous copions l'handle d'écriture du bout du pipe dans hStdOutput et hStdError puisque nous voulons que le process enfant l'utilise au lieu des handles standards de Sortie/Erreur par défaut. Nous souhaitons aussi cacher la fenêtre de console du process enfant, donc nous mettons la valeur SW_HIDE dans le membre wShowWidow. Et finalement, nous devons indiquer que les membres hStdOutput, hStdError et wShowWindow sont valables (ils sont utilisables car ils sont porteur d'une information) et doivent être utilisés en plaçant les flags STARTF_USESHOWWINDOW et STARTF_USESTDHANDLES dans le membre dwFlags.

                   invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL, addr startupinfo, addr pinfo

Nous créons maintenant le process enfant grâce à l'appel CreateProcess. Remarquez que le paramètre bInheritHandles doit être mis à TRUE pour que l'handle du pipe soit le même que son parent, et ainsi fonctionne normalement.

                       invoke CloseHandle,hWrite

Après que nous ayons créé le process enfant avec succès, nous devons fermer le bout d'écriture du pipe. Rappelez-vous que nous avons passé l'handle d'écriture au process enfant via la structure STARTUPINFO. Si nous ne fermons pas cet handle nous même, il y aura deux bouts d'écriture. Et alors le pipe ne fonctionnera pas. Nous devons fermer l'handle d'écriture : Après CreateProcess, mais aussi, Avant que nous ne lisions les données appartenant au bout de lecture du pipe.

                        .while TRUE
                            invoke RtlZeroMemory,addr buffer,1024
                            invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL
                            .if eax==NULL
                                .break
                            .endif
                            invoke SendMessage,hwndEdit,EM_SETSEL,-1,0
                            invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer
                        .endw

Maintenant nous sommes prêts à lire les données de la Sortie Standard du process enfant. Nous tournerons dans une boucle infinie jusqu'à ce qu'il n'y ait plus de données de lecture. Nous appelons RtlZeroMemory pour qu'il remplisse le buffer avec des zéros lors de l'appel ReadFile, en passant l'handle de lecture du pipe au lieu de l'handle d'un fichier (pour lire dans le Buffer et non dans un fichier). Notez bien que nous ne pouvons lire qu'un maximum de 1023 octets puisque nous avons besoin des données qu'en tant que chaîne de caractères ASCIIZ, lesquels nous pouvons maintenant passer au contrôle d'édition.
Lorsque ReadFile nous retourne les données du buffer, nous les envoyons dans le contrôle d'édition. Cependant, il y a un léger problème ici. Si nous employons SetWindowText pour mettre les données dans le contrôle d'édition, nos nouvelles données recouvriront et effaceront des données déjà existantes! Nous préférons plutôt que nos données s'ajoutent à la fin des données existantes.
Pour réussir à faire ça, nous devons d'abord déplacer notre curseur à la fin du texte dans le contrôle d'édition en envoyant le message EM_SETSEL avec wParam =-1. Ensuite, nous ajoutons les données à partir de ce point grâce au message EM_REPLACESEL.

                   invoke CloseHandle,hRead

Dès que ReadFile renvoie le NULL, nous sortons de la boucle de lecture et refermons justement l'handle de lecture.


Iczelion's Win32 Assembly Homepage


Traduit par Morgatte