IPC — Архив WASM.RU

Все статьи

IPC — Архив WASM.RU

Рано или поздно у любого человека, написавшего больше одной программы, появляется желание заставить эти программы как-то общаться между собой. Это желание со временем растет и крепнет, и в конце концов достигает таких огромных размеров, что его просто необходимо удовлетворить. Обычно такой человек в поисках удовлетворения хватается за умные книжки (кстати, по-научному обмен данными между приложениями называется interprocess communication, сокращенно - IPC), зарывается в MSDN или ищет готовые примеры в Интернете. Но что в итоге находит этот человек? Ответ на вопрос "как бы мне переслать пару байтов из одной программы в другую"? Как бы не так! Он находит в книжках множество умных фраз о мютексах, семафорах, memory-mapped-файлах и прочая, прочая, прочая. И все это нередко сопровождается многокилобайтными листингами на языке Це. Для профи, возможно, эти листинги выглядят чем-то вроде детских книжек с картинками, но человеку, впервые столкнувшемуся с проблемой коммуникации между процессами, эти коды сильно напоминают китайскую грамоту. Хотя, казалось бы, простая задача должна иметь столь же простое решение. Вот этим мы и займемся - попытаемся установить общение между программами, уложившись при этом в два десятка строчек кода.

Одним из способов обмена данными между приложениями являются сообщения (messages). В ОС Windows вместе с сообщением можно передать два четырехбайтных значения, которые называются wParam и IParam соответственно. Отправка сообщений не составляет никакой особой сложности и реализуется при помощи функций WinAPI SendMessage, SendMessageEx, PostMessage или SendNotifyMessage. Я предпочитаю использовать SendMessageEx, но в приведенном ниже примере будет использоваться исключительно SendMessage (единственно по той причине, что вызов этой функции выглядит несколько проще). Большинство сообщений Windows предназначено для отправки конкретным окнам, хотя возможна и "широковещательная" отправка сообщения всем окнам верхнего уровня, независимо от таких их атрибутов как видимость, активность и т.п. Традиционно обработка сообщений производится внутри оконной процедуры. В приложениях, не имеющих ни одного окна, для получения сообщений приходится прибегать к более сложным приемам, например, к использованию хуков. Стало быть, если мы хотим иметь возможность получать сообщения, желательно, чтобы наша программа имела хотя бы одно окно. Теперь нам осталось только определиться с "частотой волны", на которой мы будем передавать наши сообщения.

В принципе, вы можете передавать информацию при помощи сообщения WM_COPYDATA. Достоинство такого подхода в том, что это сообщение позволяет передавать объемы информации, значительно превышающие восемь байтов. Однако у этого достоинства есть и довольно неприятная обратная сторона - наверняка ваша программа далеко не единственная в системе, и кто-нибудь другой тоже мог использовать этот прием в своем софте. И если вы случайно (или умышленно) отправите некий блок данных чужой программе, невозможно предсказать, как эта программа прореагирует на такое вторжение. Если несчастная программа не рухнет сразу же с GPF, она вполне может попытаться отплатить вам аналогичным сообщением, но со своими данными - и тогда уже вашей программе придется интерпретировать информацию, которая для нее вообще-то не предназначена. В общем, безнаказанно отправить такое сообщение в "широковещательном" режиме (то есть указав в качестве хэндла окна-получателя HWND_BROADCAST) вряд ли удастся. Чтобы избежать подобных неприятностей, обычно используют уникальные идентификаторы, которые добавляют в начало или конец каждого пересылаемого блока данных - это позволяет однозначно идентифицировать приложение-отправителя сообщения и принять решение о методе обработки данных. Кроме того, перед отправкой таких сообщений, опять же, во избежание конфликтов с другими программами, приходится "вручную" производить поиск всех окон, которым будет отправлено сообщение, при помощи функции EnumWindows, что не лучшим образом сказывается на быстродействии программы. Так что если ваша задача заключается в том, чтобы просто "просигналить" программе о том, что пришла пора выполнить те или иные действия, использование столь сложного механизма вряд ли уместно.

В некоторых программах для обмена информацией используются сообщения WM_USER+N, в частности, именно так реализован механизм IPC в WinAmp. Однако Microsoft имеет по этому поводу свое особое мнение - согласно MSDN, сообщения от WM_USER+0 до WM_USER+3FFFh включительно используются только для передачи данных внутри или между окнами одного класса. А чтобы вы почем зря не нарушали данные свыше инструкции, я расскажу вам, чем может кончиться несоблюдение рекомендаций MSDN. Несколько лет назад мне встретился пакет программ, в котором для обмена информацией между отдельными приложениями использовались сообщения WM_USER+... . Сами программы работали как надо, но вот WinAmp во время их работы был совершенно непригоден к употреблению. Как только компоненты пакета начинали обмениваться информацией, сей странный mрЗ-проигрыватель просто сходил с ума. Причина была простая - все та же "широковещательная" рассылка сообщений. Так что если вы не жаждете стать невинной жертвой, в клочья разорванной ордами ВинАмпопоклонников - потрудитесь соблюдать установленный порядок.

Ну вот, теперь мы узнали самое главное - как не надо организовывать обмен данными между программами, и пусть это знание ведет нас через дебри багов и глюков к светлому будущему. Остался сущий пустяк - выяснить, как все-таки можно переслать другой программе пару байтов, никому при этом не помешав и ничего не испортив. В поисках ответа на сей глубоко философский вопрос обратите свой взор на функцию RegisterWindowMessage, которая принимает один-единственный параметр - указатель на ASCIIZ-строку, однозначно идентифицирующую сообщение. Возвращает эта функция номер сообщения, которое отныне и до окончания текущей сессии будет связано с этой строкой. При первом вызове этой функции Windows резервирует и отдает в наше полное распоряжение первое попавшееся ей на глаза свободное сообщение. Если текстовая строка уже была поставлена в соответствие какому-либо сообщению, при повторном вызове функции с тем же параметром в качестве результата мы получим именно номер этого сообщения. Образно говоря, первый вызов этой функции "открывает канал" для приема-передачи данных, а все последующие позволяют настроиться на ранее зарегистрированную "волну". Всего Windows позволяет зарегистрировать 4000h различных сообщений с номерами в интервале от 0C000h до 0FFFFh (как видите, достигнуть этого предела не так-то просто - если, конечно, специально к этому не стремиться). Но если, ценой тяжких трудов и ужасных лишений, вам все-таки удастся исчерпать этот лимит, и вы попытаетесь зарегистрировать 4001h-e сообщение, Windows вам откажет и в качестве результата функции вернет 0. К сожалению, Microsoft не предусмотрела каких-либо функций для "разрегистрации" ненужных сообщений, а потому однажды зарегистрированное сообщение будет занято в течение всей сессии.

Итак, позвольте на этом закончить теоретическую часть и перейти к самому интересному - изучению вполне работоспособного примера программы (а если быть до конца точным, то двух программ: клиента, отправляющего сообщения, и сервера, эти сообщения принимающего), демонстрирующей предлагаемую технику. Полные тексты примеров вместе с файлом проекта RadASM (для упрощения самостоятельных экспериментов) вы можете найти здесь. Автор же, в приливе нечеловеческого альтруизма и понимания, сколь трудно подчас бывает написать десяток строчек работающего кода, предоставляет всем читателям сего манускрипта право всячески цитировать код примеров и безнаказанно использовать его в своих проектах.

Оба примера в начале содержат практически идентичные участки кода, отвечающие за регистрацию сообщения, при помощи которого мы будем осуществлять коммуникацию между процессами:

  invoke RegisterWindowMessage,ADDR MsgString mov Message_for_IPC,eax .if !eax
  invoice MessageBox,0,ADDR msgMNR, ADDR rasgError,MB_APPLMODAL or MB_ICONERROR
  .else invoke DialogBoxParara,h!nstance,IDD_DIALOGl,N0LL,addr DlgProc,NULL .endif 

Нетрудно догадаться, что в этих наполненных глубинным смыслом строках мы пытаемся зарегистрировать сообщение, сохраняем полученный код для дальнейшего использования в программе и затем, в зависимости от того, удалось нам зарегистрировать сообщение или нет, вызываем диалог либо отображаем сообщение об ошибке. Теперь пришло время обратить взоры к строке MsgString, которая играет роль идентификатора сообщения. Правильный выбор строки-идентификатора имеет огромное, если не сказать судьбоносное, значение для нормальной работы программы. Вы ведь не хотите, чтобы ваша программа реагировала на каждый "чих" какой-нибудь не вашей программы, которая использует тот же идентификатор?

Конечно, вы можете взять в качестве идентификатора первую попавшуюся на глаза текстовую строку, например, название своей программы. Но практика показала, что в деле называния своих творений полет фантазии у программистов практически нулевой, повсеместно расплодившиеся PowerPad'ы и CoolClock'и - живой тому пример. Поэтому дружно проигнорируйте поданный мною дурной пример (идентификатор "Message_for_IPC" на удивление неординарен, не так ли?). Будьте оригинальнее! Например, переведите название своей программы на валлийский язык и возьмите его в качестве идентификатора. Поскольку валлийским языком во всем мире владеет всего несколько сот человек, и программистов среди них не так уж много, несчастливые совпадения названий программ весьма маловероятны. Если же вы среди всех направлений искусства программирования предпочитаете примитивизм, просто возьмите микрософтовский генератор уникальных идентификаторов, запустите его и воспользуйтесь результатом.

После того как мы узнали номер сообщения, при помощи которого наши программы будут обмениваться байтиками, мы можем смело начинать эти байтики пересылать. Более того, мы можем пересылать даже сдвоенные и счетверенные байтики, а если как следует постараться - то и все восемь байтов! Дурное дело, как известно, нехитрое:

  invoke SendMessage,HWND_BROADCAST,Message_for_IPC, wParam ,IParam 

Байтики, которые мы хотим переслать, нужно всего лишь "зарядить" в качестве параметров wParam и IParam, каждый из которых имеет тип DWORD. В моем примере принципиальной необходимости во втором параметре не было, поэтому он равен нулю. В результате все окна верхнего уровня получат "подарочек" в виде нашего сообщения и двух его параметров. В нашем примере мы отправляем в качестве параметра номер нажатой кнопки, который вычисляем как ID кнопки минус 1000:

  .elseif (eax==IDC_BTNl) || (eax==IDC_BTN2) || (eax==IDC_BTN3) sub eax,1000
  invoke SendMessage,HWND_BROADCAST,Message_for_IPC,eax,0 .endif 

Теперь посмотрим, как получить отправленные таким образом данные в своей программе. В принципе, тут тоже нет ничего сложного - обычная обработка сообщения в оконной процедуре:


  mov eax,uMsg .if eax== Message_for_IPC 
; Если получено зарегистрированное нами сообщение
 invoke wsprintf,
          ADDR Buffer,
          ADDR FratStr,
          wParam
 invoke SetDlgItemText,hWin,IDC_STC1,ADDR Buffer 

Как видно из текста, здесь мы всего лишь переводим номер нажатой кнопки в текстовое представление и выводим полученный текст в соответствующее окно.

Вот и все! Впишите приведенные куски кода в свои программы, и проблемы с обменом байтиками между процессами у вас больше не возникнет. Желающие могут даже посчитать, сколько строчек текста нам на это потребовалось, и в какое количество байтов эти строчки откомпилировались. Ну и, конечно, в очередной раз восхититься языком, который предоставляет возможность писать самые короткие программы.

2002-2013 (c) wasm.ru