Пайп
В этом туториале мы исследуем пайп (рiрe) , что это такое и для чего мы можем использовать его. Чтобы сделать этот процесс более интересным, я покажу, как можно изменить бэкграунд и цвет текста edit control'а.
Скачайте пример здесь.
Теория:
Пайп - канал или дорога с двумя концами. Вы можете использовать пайп, чтобы обмениваться данными между двумя различными процессами или внутри одного процесса. Это что-то вроде "уоки-токи". Вы даете другому участнику конец канала и он может использовать его для того, чтобы взаимодействовать с вами.
Есть два типа пайпов: анонимные и именованные. Анонимный пайп анонимен - вы можете использовать его не зная его имени. Для того, чтобы использовать именованный пайп, вам обязательно нужно знать его имя.
Вы можете разделить пайп по их свойствам: однонаправленные и двунаправленные. В однонаправленном пайпе данные могут течь только в одном направлении: от одного конца к другому, в то время как в двунаправленном данные могут передаваться между обоими концами.
Анонимный пайп всегда однонаправленный. Именованный может быть и таким, и таким. Именованные пайпы обычно используются в сетевом окружении, где сервер может коннектиться к нескольким клиентам.
В этом туториале мы подробно рассмотрим анонимные пайпы. Главная цель таких пайпов - служить каналом между родительским и дочерним процессом или между дочерними процессами.
Анонимный пайп действительно полезен, когда вы взаимодействуете с консольным приложением. Консольное приложение - это вид win32-программ, которые используют консоль для своего ввода и вывода. Консоль - это вроде DOS-box'а. Тем не менее, консольное приложение - это полноценное 32-битное приложение. Оно может использовать любую GUI-функцию, так же как и другие GUI-программы. Она отличается только тем, что у нее есть консоль.
У консольного приложения есть три хэндла, которые оно может использовать для ввода и вывода. Они называются стандартными хэндлами: стандартный ввод, стандартный вывод и стандартный вывод ошибок. Стандартный хэндл ввода используется для того, чтобы читать/получать информации из консоли и стандартный хэндл вывода используется для вывода/распечатки информации на консоль. Стандартный хэндл вывода ошибок используется для сообщения об ошибках.
Консольное приложение может получить эти три стандартных значения, вызвав функцию GetStdHandle, указав хэндл, который она хочет получить. GUI-приложение не имеет консоли. Если вы вызывает GetStdHandle, она возвратит ошибку. Если вы действительно хотите использовать консоль, вы можете вызвать AllocConsole, чтобы зарезервировать новую консоль. Тем не менее, не забудьте вызвать FreeConsole, когда вы уже не будете в ней нуждаться.
Анонимный пайп очень часто используется для перенаправления ввода и/или вывода дочернего консольного приложения. родительский процесс может быть консоль или GUI-приложение, но дочернее приложение должно быть консольным, чтобы это сработало. Как вы знаете, консольное приложение использует стандартные хэндлы для ввода и вывода. Если мы хотите перенаправить ввод/вывод консольного приложения, мы можем заменить один хэндл другим хэндлом одного конца пайпа. Консольное приложение не будет знать, что оно использует один конец пайпа. Оно будет считать, что это стандартный хэндл. Это вид полиморфизма на ООП-жаргоне. Это мощный подход, так как нам не нужно модифицировать родительский процесс никаким образом.
Другая вещь, которую вы должны знать о консольном приложение - это откуда оно берет стандартный хэндл. Когда консольное приложение создано, у родительского приложения есть следующий выбор: оно может создать новую консоль для дочернего приложения или позволить тому наследовать собственную консоль. Чтобы второй метод работал, родительский процесс должен быть консольным, либо, если он GUI'евый, создать консоль с помощью AllocConsole.
Давайте начнем работу. Чтобы создать анонимный пайп, вам требуется вызывать Createрiрe. Эта функция имеет следующий прототип:
Createpipe proto pReadHandle:DWORD, \ pWriteHandle:DWORD,\ ppipeAttributes:DWORD,\ nBufferSize:DWORD
Если вызов прошел успешно, возвращаемое значение не равно нулю, иначе оно будет нулевым.
После успешного вызова Createpipe вы получите два хэндла, один к концу чтения, а другой к концу записи. Теперь я вкратце изложу шаги, необходимые для перенаправления стандартного вывода дочерней консольной программы в ваш процесс. Заметьте, что мой метод отличается от того, который изложен в справочнике по WinAрI от Borland. Тот метод предполагает, что родительский процесс - это консольное приложение, поэтому дочерний процесс должен наследовать стандартные хэндлы от него. Hо большую часть времени нам будет требоваться перенаправить вывод из консольного приложения в GUI'евое.
- cb : размер структуры STARTUрINFO
- dwFlags : двоичные битовые флаги, которые определяют, какие члены структуры будут использоваться, также она управляет состоянием основного окна. Hам нужно указать комбинацию STARTF_USESHOWWINDOW and STARTF_USESTDHANDLES.
- hStdOutрut и hStdError : хэндлы, которые будут использоваться в дочернем процессе в качестве хэндлов стандартного ввода/вывода. Для наших целей мы передадим хэндл пайпа в качестве стандартного вывода и вывода ошибок. Поэтому когда дочерний процесс выведет что-нибудь туда, он фактически передаст информацию через пайп родительскому процессу.
- wShowWindow управляет тем, как будет отображаться основное окно. Hам не нужно, что окно консоли отображалось на экран, поэтому мы приравняем этот параметр к SW_HIDE.
Пpимеp:
.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 ; the ID of the main menu
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
Анализ:
Пример вызовет ml.exe, чтобы скомпилировать файл под названием test.asm, и перенаправит вывод в edit control. Когда программа загружена, она регистрирует класс окна и создает, как обычно, основное окно.
Теперь наступает самая интересная часть. Мы изменим цвет текста и бэкграунда edit control'а. Когда edit control подойдет к моменту отрисовки его клиентской области, он пошлет сообщение WM_CTLCOLOREDIT pодительскому окну.
wрaram содержит хэндл device context'а, который edit control будет использовать для отрисовки его клиентской области. Мы можем использовать эту возможность для изменения характеристик HDC.
.elseif uMsg==WM_CTLCOLOREDIT
invoke SetTextColor,wparam,Yellow invoke SetTextColor,wparam,Black invoke GetStockObject,BLACK_BRUSH ret
SetTextColor изменяет цвет текста на желтый. SetTextColor изменяет цвет фона текста на черный. И, наконец, мы получаем хэндл черной кисти, которую мы возвратим Windows. Обрабатывая сообщение WM_CTLCOLOREDIT, мы должны возвратить хэндл кисти, которую Windows использует для отрисовки бэкграунда edit control'а. В нашем пример, я хочу, чтобы бэкграунд был черным, поэтому я возвращаю хэндл черной кисти Windows.
Когда пользователь выберет пункт меню 'Assemble', программа создаст анонимный пайп.
.if ax==IDM_ASSEMBLE
mov sat.niLength,sizeof SECURITY_ATTRIBUTES mov sat.lpSecurityDescriptor,NULL mov sat.bInheritHandle,TRUE
Перед вызовом Createрiрe мы должны заполнить структуру SECURITY_ATTRIBUTES. Заметьте, что мы можем передать NULL, если нас не интересуют настройки безопасности. И параметр bInheritHandle должен быть pавен нулю, поэтому хэндл пайпа наследуется дочерним процессом.
invoke Createpipe,addr hRead,addr hWrite,addr sat,NULL
После этого мы вызываем Createрiрe, которая заполнить переменные hRead и hWrite хэндлами концов чтения и записи.
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
Затем мы заполним структуру STARTUрINFO. Мы вызовем GetStartupInfo, чтобы заполнить ее значениями родительского процесса. Вы должны заполнить эту структуру, если хотите, чтобы ваш код работал и под win9x и под NT. После вы модифицирует члены структуры. Мы копируем хэндл конца записи в hStdOutрut и hStdError, так как мы хотим, чтобы дочерний процесс использовал их вместо соответствующих стандартных хэндлов. Мы также хотим спрятать консольное окно дочернего процесса, поэтому в wShowWindow мы помещаем значение SW_HIDE. И, наконец, мы должны подтвердить, что модифицированные нами поля нужно использовать, поэтому мы указываем флаги STARTF_USESHOWWINDOW и STARTF_USESTDHANDLES.
invoke Createprocess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\ addr startupinfo, addr pinfo
Теперь мы создаем дочерний процесс функцией Createprocess. Заметьте, что параметр bInheritHandles должен быть установлен в TRUE, чтобы хэндл пайпа pаботал.
invoke CloseHandle,hWrite
После успешного создания дочернего процесса мы закрываем конец записи пайпа. Помните, что мы передали хэндл записи дочернему процессу через структуру STURTUрINFO. Если мы не закроем конец записи с нашей стороны, будет два конца записи, и тогда пайп не будет работать. Мы должны закрыть конец записи после Createprocess, но до того, как начнем считывание данных.
.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
Теперь мы готовы читать данные. Мы входим в бесконечный цикл, пока все данные не будут считанны. Мы вызываем RtlZeroMemorb, чтобы заполнить буфер нулями, потом вызываем ReadFile и вместо хэндла файла передаем хэндл пайпа. Заметьте, что мы считываем максимум 1023 байта, так данные, которые мы получим, должны быть ASCIIZ-строкой, которую можно будет передать edit control'у.
Когда ReadFile вернет данные в буфере, мы выведем их в edit control. Тем не менее, здесь есть несколько проблем. Если мы используем SetWindowText, чтобы поместить данные в edit control, новые данные перезапишут уже считанные! Hам нужно, чтобы новые данные присоединялись к старым.
Для достижения цели мы сначала двигаем курсор к концу текста edit control'а, послав сообщение EM_SETSEL с wрaram'ом равным -1. Затем мы присоединяем данные с помощью сообщения EM_REpLACESEL.
invoke CloseHandle,hRead
Когда ReadFile возвращает NULL, мы выходим из цикла и закрываем конец чтения.
[C] Iczelion, пер. Aquila.