Лептонный стиль программирования — Архив WASM.RU

Все статьи

Лептонный стиль программирования — Архив WASM.RU

ЧАСТЬ ПЕРВАЯ. ПОСТАНОВКА ЗАДАЧИ

Лептоны (от греч. leptos, что значит "легкий". Ср."лепта" - мелкая разменная монета. Напр. "внести свою лепту" - сделать дела на копейку, а потом орать, что потратился на рубль) - это такие ма-а-асенькие элементарные частицы.

Существуют две теории лептонов: правильная и неправильная.

НЕПРАВИЛЬНАЯ ТЕОРИЯ ЛЕПТОНОВ

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

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

Это богоданное свойство хорошо было наблюдать летом 2000 года в очереди к мощам Св.Пантелеймона в Храме Христа Спасителя в Москве. Бесконечной вереницей стояли русские люди по 12-15 часов в надежде приобщиться святости великомученика и целителя, и такие же русские люди бесконечно обходили стоящих, собирая деньги якобы на лекарства, надгробия и восстановление храмов, и такие же русские люди с дракой и бранью прорываются в храм без очереди.

Одним из самых известных Адронов наших дней является знаменитый кинорежиссер Андрон Михалков-Кончаловский, отпрыск древнего дворянского рода Михалковых, известных своей неуемной творческой предприимчивостью, близостью к царям и неиссякаемой мужской силой. Самый популярный фильм Михалкова-Кончаловского - "Танго и Кэш", шедший в прокате города Красноголозадовска (бывш. Бухаринград) под названием "Кто девушку платит, тот ее и танцует".

Лептонов известно науке всего двенадцать штук. Точнее шесть, а остальные шесть - это их античастицы: электрон, мюон, тау-лептон, и три нейтрино: электронное, мюонное и, как вы уже можете догадаться, тау.

Нейтрино - вообще очень интересная штучка. Есть подозрения, что на самом деле Вселенная состоит в основном-то как раз из нейтрино. На миллиард нейтрино приходится одна какая-нибудь другая ма-а-асенькая элементарная частица. И основной вклад в мировую гравитацию вносят именно они - нейтрино. И если вдруг однажды мы доживем до Большого Чпока (процесса, обратного Большому Взрыву), то именно они будут в нем виноваты, свернув своей массой Вселенную обратно в сингулярность, из которой она когда-то родилась. И тогда у Кого-то на экран выскочит синяя маска смерти с предложением нажать Ctrl+Alt+Del. Но это будет уже совсем другая история.

Данная теория лептонов названа неправильной, потому что с точки зрения программирования она совершенно бесплодна.

ПРАВИЛЬНАЯ ТЕОРИЯ ЛЕПТОНОВ

Лептоны - это элементарные частицы мировой информации. Они принципиально неуловимы никакими физическими приборами. Скорость света - это недостижимая для них минимальная скорость, а обычно они движутся гораздо быстрее. Лептонное поле хранит информацию о всех прошедших, настоящих и будущих событиях в любой точке пространства-времени, включительно от чиха простудившегося этим летом микроба до мановения пальца Любимого Руководителя Товарища Ким Чен Ира, указывающего Владимиру Владимировичу направление движения России в сторону окончательной победы идей Чучхе.

В указанном промежутке лежат также разработка ОС Windows, грядущее столкновение нашей Галактики с галактикой Андромеды, передача НТВ Газпрому за долги, хрущовские испытания 50-мегатонной бомбы на Новой Земле, день рождения в прошлую субботу, раскаяние в прошлое воскресенье и многие другие события, которые здесь следовало бы перечислить, но неохота.

Единственный прибор, способный взаимодействовать с лептонами - это человеческая душа, поскольку сама она суть лептонный сгусток. Все, что для нас проявляется как интуиция, озарения, идеи - это тривиальные результаты взаимодействия души с лептонным полем (см., например, песню группы "Любэ" Улочки Московские, где есть такие пророческие слова: "Эх, сегодня я, кажись, надерусь"). Это, так сказать, непроизвольные формы взаимодействия. Существуют также и произвольные формы, то есть взаимодействия, совершаемые человеком сознательно: молитва, медитация, камлание, гадание, транс, сеанс Кашпировского, выборы президента и пр. Как правило, целью произвольных форм взаимодействия является получение из лептонного поля интересующей информации или попытка изменить его конфигурацию желательным для себя образом.

Здесь мы прекратим рассмотрение теорий лептонов, потому что все, что нужно, уже выяснили, и перейдем к практике программирования.

Среди традиционных проблем ассемблерного программиста связывание занимает далеко не последнее место.

Поясняю. Всякий сколько-нибудь значительный проект обычно содержит более одного модуля, то бишь файла с исходным текстом, по той простой причине, что будучи запихнутым в один файл, проект становится неуправляемо громоздким. Модуль еще иногда называют единицей трансляции, имея в виду то обстоятельство, что обрабатывая его и превращая в объектный файл, компилятор ни сном, ни духом не ведает о существовании других модулей. Своевременное и логически обоснованное разбиение проекта на модули столь важно, что некоторые авторы даже склонны рассматривать модульность как отдельную парадигму программирования, наряду с хаотической, процедурной, структурной и объектно-ориентированной (Собоцинский В.В. Практический курс Турбо С++. М, "Свет",1993).

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

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

  1. Команды, содержащиеся в одних модулях, обращаются к переменным, описанным в других модулях, то есть читают или пишут данные в память, именованную этими переменными
  2. Команды call, содержащиеся в одних модулях, вызывают процедуры, тела которых содержатся в других модулях.
Возможен еще такой способ взаимодействия, как передача управления из одного модуля в другой командой jmp или ее условными формами (je, jb, js и пр.). Но, если только программист не пытается реализовать какую-нибудь специальную идею, применение этого способа следует считать проявлением запредельного случая неправильного стиля программирования.

В Windows существует еще один способ взаимодействия модулей: когда из одних модулей посылаются сообщения окнам, созданным в других модулях. Но об этом ниже.

Стоит проблема: как позволить командам из одного модуля обращаться к данным, описанным в других модулях. Или как из одного модуля вызывать процедуры, тела которых содержатся в других модулях. Решение этой проблемы как раз и называется связыванием.

Родной для ассемблерных программ способ связывания обеспечивается применением сладкой парочки директив PUBLIC и EXTRN (она же EXTERN). Директива PUBLIC объявляет имя переменной или процедуры доступным для обращения из других модулей. Директива EXTERN сообщает компилятору, что команды данного модуля могут обращаться к имени переменной или процедуры, описанной в другом модуле.

Например:

в модуле masha.asm содержится такой фрагмент кода:

PUBLIC mashin_vozrast
.data
mashin_vozrast dd 17

а в модуле vasya.asm содержится такой фрагмент кода:

EXTRN mashin_vozrast:dword
.code
;...
 cmp mashin_vozrast,18
;...
Здесь дан наиболее часто применяемый вариант директив PUBLIC и EXTRN. На самом деле их возможности несколько шире, но они не являются предметом этой статьи. Читайте документацию на соответствующий вашим предпочтениям ассемблер.

Обратите внимание, что переменная mashin_vozrast объявлена как двойное слово, что вполне естественно для программирования под win32, даже при том условии, что Маша вряд ли достигнет возраста 4294967296 лет, сколько бы здоровья мы ей ни желали. Настоятельно рекомендуется в подобных случаях (когда числовая переменная объявлена как одиночная, а не как массив) всегда использовать переменные размером в двойное слово, потому что обращение к таким переменным из 32-разрядного кода существенно более компактно, чем к переменным размером в слово или байт.

Как видим, Вася пытается оценить, насколько близкими могут быть его отношения с Машей, чтобы не загреметь по очень неприятной статье. Его, вероятно, ждет разочарование, но только не на этапе компиляции, потому что компилируется он программой-компилятором (например, ml.exe) в одиночку, без участия предмета своих вожделений.

Связыванием же занимается компоновщик, он же линкер, он же link.exe. Это его основная задача, откуда и произошло его название. Схватив полученные в результате двух актов компиляции объектные файлы masha.obj и vasya.obj, он обнаруживает, что Маша публично заявляет о своем возрасте (впрочем, 17-ти лет стесняться не приходится), а Вася внимательно прислушивается. Компоновщик удовлетворяет обоих, выделяя в исполняемом модуле sex.exe место под переменную размером в двойное слово, инициализирует ее числом 17, а в команду cmp, с помощью которой Вася интересуется Машиным возрастом, подставляет адрес этой переменной.

Связывание вызовов процедур выглядит ненамного сложнее:

модуль masha.asm:

PUBLIC get_masha
.code
get_masha PROC
 ;...
 ret
get_masha ENDP

модуль vasya.asm:

EXTRN get_masha:near
.code
;...
call get_masha
;...
Обратите внимание, что процедура get_masha объявлена как ближняя. В win32, поскольку дело происходит в плоской (flat) модели памяти, все процедуры ближние.

Как видим, Вася добрался-таки до Маши. Вероятно, в какой-то момент переменная mashin_vozrast инкрементировалась, или любовь оказалась сильнее условностей. Но так или иначе связывание состоялось.

Сказанное выше описывает стандартный для ассемблера механизм связывания на этапе компоновки приложения. По сути своей это так называемое раннее связывание, то есть связывание, выполняемое во время разработки программы. В отличие от позднего, используемого, например, в C++ для виртуальных функций, что является основой для реализации полиморфизма - одного из столпов объектно-ориентированного программирования.

Есть ли у этого метода недостатки? Да до... в общем, вам по пояс будет.

Представьте себе, что вы коренной русак без малейшей примеси немецкой крови. Тогда, видимо, вы вряд ли являетесь поклонником такого понятия, как инкапсуляция, и глобальных переменных в ваших программах - как блох на мамонте. А каждая глобальная переменная - это одна директива PUBLIC и энное количество директив EXTRN, по одной на каждый модуль, из которого на нее ссылаются. Поменяли имя переменной - будьте любезны править все модули. А если у вас, допустим, сто глобальных переменных? Не слабо?

А даже если вы и наоборот, запредельный педант, и пипифакс рвете исключительно по перфорации, и объектная ориентация для вас важнее, чем сексуальная? Тогда, вероятно, глобальных переменных в ваших программах нет совсем, а межмодульный обмен данными вы обеспечиваете с помощью специальных интерфейсных процедур, на манер публичных функций-членов классов в C++. Ну и что, намного легче вам иметь длиннющие списки директив PUBLIC и EXTRN для процедур, чем для переменных?

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

Отвечая на первый попавшийся FAQ, объясним сразу, при чем же здесь все-таки лептоны и почему стиль назван так претенциозно, если не сказать помпезно.

Все началось несколько лет назад, когда мы только-только начинали программировать под Windows. Почти сразу выяснив, что одно из главных достоинств ассемблерных программ - максимальное быстродействие - уже больше не является безусловным благодаря многозадачной архитектуре операционной системы, мы сначала опечалились, а потом, подумав, решили, что это даже хорошо. Можно расслабиться, перестать высчитывать загрузку конвейера процессора и позволить себе некоторые излишества: размочить позавчерашний сухарь водичкой, постирать носовой платок, не буферизировать файлы размером меньше кластера, заглянуть на Интердаму, наконец.

Особенно большое впечатление произвел механизм сообщений Windows. Допустим, нажимаете вы, не подумав, мышкой на кнопку в диалоговом окне. Внешне все происходит просто: нажимается кнопка "Нет" в диалоге "Хотите ли вы сохранить документ, над которым вы работали последние четыре часа"; вы страшно богохульничаете, плюете в монитор и с горя отправляетесь в магазин. Вот и все, и большую часть из этой процедуры вы выполняете вообще без помощи компьютера.

А на самом деле? На самом деле все ужасно сложно и долго. Драйвер мыши обнаруживает судорожное движение вашего пальца. Потом он интересуется местоположением курсора на экране. Полученные координаты он отправляет операционной системе на предмет определения окна, над которым в данный момент находится курсор. А окон открыта целая туча: всякие там кнопочки-шмопочки, документики-шмокументики, списочки-шмисочки. И все перекрываются, и каждое норовит чего-нибудь обработать. И вот операционная система перебирает их все и находит то, которое в точке курсора лежит поверх всех остальных. И отправляет ему сообщение. Методом Pоst между прочим, то есть ставя его в очередь, а не вызывая непосредственно оконную процедуру. Сообщение отправляется родительскому окну и торчит там в очереди, пока не придет пора его обработать. Кроме того, поскольку дело происходит в диалоговом окне, кнопочка еще и перерисовывается, отображая нажатое состояние. А потом еще удаляется с экрана диалоговое окно, и перерисовывается то окно, в котором находился ваш документ, а потом удаляется и оно, и перерисовываются все лежавшие под ним другие окна, при этом тоже не особенно экономя процессорное время и память. И все это сопровождается передачей такого количества сообщений, что Смольный в ночь с 24 на 25 октября (с.с.) 1917 года по сравнению с этим гвалтом покажется вам санаторием ЦК КПСС во время послеобеденного тихого часа, когда пациенты переваривают телятину по-кабардински под белым соусом в сопровождении нежных трапецеидальных импульсов стимулирующих перистальтику кремлевских таблеток. Короче, рассмотрев эту предельно упрощенную схему действий, мы понимаем, что вместо того, чтобы просто уничтожить все ваши труды, ОС сама долго и плодотворно трудится.

И что же, среди этого разврата мы будем экономить, отказывая себе во всем, даже в полкило красной икры на завтрак?

Да никогда!

Мы построим свою программу из модулей. Каждому модулю мы поручим выполнение какой-нибудь конкретной задачи в рамках общего дела. Один пусть занимается работой с настройками: содержит информацию об активных панелях управления, составе кнопок на них и пр., и предоставляет эту информацию другим модулям по их запросам. Другой модуль пусть обеспечивает сохранение состояния приложения в период между его запусками: запоминает размер и положение окон, имена редактировавшихся документов, списки изменяемых данных и еще что-нибудь. И тоже предоставляет эту информацию другим модулям по запросам. Третий модуль пусть обслуживает строку статуса: инициализирует ее, разбивает на поля, показывает в них то или иное содержимое по запросам других модулей. Четвертый модуль пусть предоставляет текстовые строки на выбранном пользователем языке. Пятый модуль...

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

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

Отличие же лептонного стиля состоит в том, что запрашивающий модуль-клиент не обязан знать, и не может знать, и вовсе даже не интересуется, кто из существующих модулей-серверов обслуживает его запрос, и вообще, существует ли такой сервер.

В этом - квинтэссенция лептонного стиля. Клиент отправляет свой запрос в пространство, не адресуясь ни к кому конкретному, наподобие молитвы всем богам. "Эй, кто-нибудь, кто может, ответьте, пожалуйста, какова сейчас ширина главного окна приложения?" - как бы кричит в эфир модуль, создающий, например, подчиненное окно, занимающее одну восьмую часть главного окна по горизонтали. И получает в ответ неизвестно от кого: "720 пикселов, дружище! Пользуйся на здоровье!".

Теперь ответим на второй первый попавшийся FAQ. Тянет ли эта идея на понятие "стиль программирования"? По-нашему, тянет. Ведь что есть стиль программирования? Это более-менее стабильный набор приемов организации программы и оформления исходного текста, используемый программистом с целью облегчения ее разработки и понимания. С точки зрения этого определения то, о чем мы сейчас говорим - именно и есть стиль. Стабилен ли он? Да вы только попробуйте его хотя бы разок, и обратно уже не запроситесь. Идет ли в данном случае речь об организации программы? Безусловно: и с формальной точки зрения (отсутствие директив PUBLIC/EXTRN), и с фактической (организация специальной системы обмена сообщениями внутри программы) - это инструмент организации программы. Идет ли в данном случае речь об оформлении исходного текста? Конечно, да. Далее мы покажем, как с помощью макросов удобно и наглядно записываются запросы клиентов.

Первая реализация поддержки лептонного стиля программирования была прямо выполнена на штатных средствах обмена сообщениями Windows. До сих пор еще некоторые наши программы используют ее. Общая идея там была такова: при запуске приложения создавалось невидимое окно, называемое нами супервизором, дескриптор которого объявлялся глобальным для всего проекта. Оконная процедура супервизора, таким образом, становилась доступной из любой точки программы для передачи (SendMessage) или посылки (PostMessage) пользовательских сообщений, то есть относящихся к диапазону WM_USER (00400h...07fffh) и/или диапазону WM_APP (08fffh...0bfffh):

invoke SendMessageA,hWnd,Msg,wParam,lParam 

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

  1. если в каком-то месте программы требовалось обратиться к лептонному сервису, вызывалась функция API SendMessage (реже, при необходимости - PostMessage), при этом в качестве окна-получателя (hWnd) указывался супервизор. Сообщение (Msg) определяло смысл запроса, а два параметра (wParam и lParam) - его параметры;
  2. оконная процедура супервизора, получив это сообщение, поочередно транслировала его всем доступным в приложении диспетчерам модулей, передавая им параметры, например, через стек;
  3. очередной диспетчер модуля, получив вызов от супервизора, определял, обрабатывается ли он этим модулем. Если не обрабатывается, то модуль возвращал управление супервизору с признаком "не обработано". Если обрабатывается, то передавал его на обработку и, по ее завершении, возвращал ответ в регистре eax с признаком "обработано";
  4. супервизор, получив от какого-нибудь очередного модуля ответ с признаком "обработано", прекращал опрос модулей и завершал оконную процедуру, возвращая ответ в регистре eax;
  5. запрашивавший модуль считывал ответ из регистра eax.

Как параметры (wParam и lParam), так и ответ (eax) могли представлять собой непосредственные данные, если они помещались в размер двойного слова, либо являться адресами (указателями) блоков памяти (структур, строк), в которые модуль-клиент и/или модуль-сервер помещали соответствующие данные. В каждом конкретном случае это определялось смыслом запроса, то есть значением сообщения (Msg).

У этой схемы, при ее общей очевидной работоспособности, имелись несколько неприятных недостатков (мы настаиваем на этом определении, ибо всякий поживший на свете человек знает, что некоторые недостатки иногда бывают очень даже приятны):

  1. для своих внутренних утилитарных целей мы задействуем весь монструозный механизм сообщений Windows. (Васин элементарный запрос MASHA_GOTO_POGULYAT_NA_CHERDAK, не сомневайтесь, будет в подробностях обсосан и переварен всем двором и уж точно не минует машиного батяню-шоферюгу с вот такенными кулачищами.) Конечно, душа настоящего ассемблерщика станет невыносимо страдать при каждом таком запросе. А вы представляете, если запросы попрут, допустим, по сто штук в секунду? Прямая дорога либо к инфаркту миокарда, либо к циррозу печени;
  2. очередь оконных сообщений начинает жить только после создания окна супервизора, когда заработает цикл GetMessage-TranslateMessage-DispatchMessage. А между тем, очень часто до того хочется провести некоторое количество операций по инициализации приложения. И очень хочется выполнить их тоже в лептонном стиле. Почему бы и нет?
  3. два входных параметра - это, конечно, больше, чем ни одного, но заметно меньше, чем три или более. То есть маловато будет в весьма многих случаях. А ничего не поделаешь: таков формат системного сообщения Windows. Можно, конечно, дополнительно использовать регистры esi и edi, которые, как известно, сохраняются функциями API, но это откровенный паллиатив;
  4. параметры передаются через стек, и поэтому их всегда два. Даже если не нужно ни одного. Налицо преступное разбазаривание кода, стека и процессорного времени;
  5. 48128 сообщений, зарезервированных в диапазонах WM_USER и WM_APP - это не так уж много, неправда ли? Всего каких-нибудь 20 лет работы над программой, и вы их исчерпаете. Что будете делать тогда? Куда лучше было бы иметь в запасе 232 сообщений. Тогда можно было бы более-менее спокойно работать чуть более 1784810 лет, что в большинстве случаев следует признать достаточным.

Поэтому мы не станем рассматривать здесь вариант реализации лептонного стиля на базе системы сообщений Windows, а сразу перейдем к более прогрессивному - автономному варианту. Его отличительной особенностью является применимость не только под Windows, а в любой ассемблерной программе, в том числе под DOS, Linux и пр. Если он вам понравится, вы можете использовать приведенный здесь код один к одному. Он вполне работоспособен и достаточно функционален. Но еще правильнее воспринимать его как набор идей, которые вы свободно можете развить и дополнить так, как посчитаете нужным. Или, например, перевести на другой язык программирования.

Реализация и заключительные замечания - во второй части статьи.

ЧАСТЬ ВТОРАЯ. РЕАЛИЗАЦИЯ

В реализации задействованы файлы:

  • @struct.inc - файл глобальных макросов. Это макросы, с помощью которых программист улучшает свою рабочую среду. каждый может придумать их великое множество. Некоторые из используемых нами вы можете найти в проекте MyCall. Важно, что этот файл следует включать во все модули проекта.
  • globals.inc - файл глобальных констант проекта. В нем вы будете вести список констант, представляющих молитвы (лептонные вызовы). Он также должен быть включен во все модули проекта, что позволит выполнять любой лептонный вызов из любой точки проекта. Впрочем, возможен вариант, когда молитвы могут быть разбиты на группы по назначению. В таком случае все группы молитв должны быть доступны только супервизору, а каждому из модулей - только те группы, молитвы из которых используются в этом модуле. Такая организация потребует от программиста дополнительных накладных расходов на администрирование. В то же время заманчивым может оказаться сокращение времени на компиляцию проекта. Ведь если файл global.inc один на всех, то при добавлении в него каждой новой молитвы (то есть довольно часто) автоматически будет вызываться полная перекомпиляция всех модулей проекта (если, конечно, вы правильно настроили MS DevStudio). Выбор - за вами. Со своей стороны скажем, что по причине врожденной лености ни разу не пытались разбивать молитвы на группы. На современном компьютере MASM работает достаточно быстро, можно и потерпеть;
  • 00_main.asm - главный модуль проекта - тот, который содержит процедуру WinMain (см. также статью Минимальное приложение). У нас он теперь будет содержать еще и супервизор;
  • XX_*.asm - все остальные модули проекта. Здесь XX - двухсимвольный идентификатор модуля, в котором каждый симол X может быть цифрой или буквой латинского алфавита, а "*" - произвольный набор символов, смысл которого нужен человеку, но не компилятору (например: 01_main_window.asm - модуль обслуживания главного окна). Каждый такой модуль, если только он собирается выступать в качестве лептонного сервера для молитв других модулей, обязан иметь в своем составе процедуру диспетчера.

Ниже приведены фрагменты кода, включаемые в перечисленные файлы и обеспечивающие поддержку лептонного стиля программирования, и комментарии к ним.

Файл глобальных макросов @stuct.inc:

;------------------------- эквиваленты параметров молитвы
@par0	EQU esi
@par1	EQU edi
@par2	EQU edx
@par3	EQU ecx
;------------------------- макрос молитвы
@pray MACRO pray,par0,par1,par2,par3
 IFNB 
  mov esi,par0
 ENDIF
 IFNB 
  mov edi,par1
 ENDIF
 IFNB 
  mov edx,par2
 ENDIF
 IFNB 
  mov ecx,par3
 ENDIF
 invoke supervisor,pray
ENDM
;------------------------- макрос формирования списка диспетчеров
@dispatchers MACRO
 IFDEF MODULES
  @id_offset=1
  @id_size SIZESTR MODULES
  :next
   IF @id_offset GT @id_size
    EXITM
   ENDIF
   @module_id SUBSTR MODULES,@id_offset,2
   @module_name CATSTR ,@module_id
   @module_name PROTO :DWORD
   dd offset(@module_name)
   @id_offset=@id_offset+3
  GOTO next
 ENDIF
ENDM
;------------------------- директивы определения модели
.386
.Model flat,stdcall
;------------------------- прототип супервизора
supervisor PROTO :DWORD
;------------------------- идентификатор модуля
@module SUBSTR @FileName,1,2
;------------------------- конструкции, зависимые от модуля
IFIDN @module,<00>			;если это модуль супервизора
 .const					;создать список диспетчеров
  dispatchers dd 0 dup(0)
              @dispatchers	;вызов макроса формирования списка диспетчеров
              dd 0
ELSE				;если это обычный модуль
 @dispatcher CATSTR ,@module	;сконструировать имя диспетчера
ENDIF

Пояснения:

  1. Эквиваленты параметров молитвы используются внутри диспетчеров (см.ниже в файле XX_*.asm). Они нужны для того, чтобы у программиста не было необходимости помнить, какой параметр передается в каком регистре. Символ "@" напоминает программисту о том, что это регистры, а не адреса памяти.
  2. Макрос молитвы используется для формирования лептонных вызовов из программы - молитв и бродкастов, например:
    @pray P_GET_STRING,offset(string_buffer),buffer_size,string_id
    
    Этот текст после компиляции будет преобразован в последовательность команд:
    mov esi,offset(string_buffer)
    mov edi,buffer_size
    mov edx,string_id
    push P_GET_STRING
    call supervisor
    
    Видно, что в отличие от описанной выше реализации но основе средств обмена сообщениями Windows, этот код оптимален: кодируются только те параметры, которые используются. Другие примеры молитв и бродкастов:
    @pray B_START,hinstance
    @pray P_SET_MAIN_WINDOW_TITLE,offset(main_window_name)
    @pray P_STORE_STATUS,
       STATUS_MAIN_WINDOW_POSITION,
       offset(main_window_rectangle),
       show_status 
    @pray B_STOP
    
  3. Макрос формирования списка диспетчеров используется только один раз - при компиляции главного модуля. На основании списка идентификаторов модулей MODULES (см. ниже в файле 00_main.asm) он создает в константном сегменте приложения таблицу адресов процедур диспетчеров всех модулей. Таблица завершается нулевым значением. При каждом лептонном вызове супервизор поочередно передает управление по адресам из этой таблицы, вызывая, таким образом, диспетчеры модулей.
  4. Прототип супервизора обеспечивает связывание лептонных вызовов из модулей с процедурой супервизора, описанной в главном модуле 00_main.asm.
  5. Идентификатор модуля представляет собой два первых символа имени asm-файла. Можно придумать и другие варианты, но нам показался привлекательным этот. Он удобен еще и тем, что в панели Workspace рабочей среды MS Developer Studio именованные таким образом файлы располагаются в строгом порядке, так как происходит их сортировка по имени.
  6. В зависимости от того, какой модуль приложения компилируется - главный или обычный, - в нем либо создается список диспетчеров, либо конструируется имя диспетчера. Имя диспетчера, будучи глобальным, должно быть уникально в пределах проекта. Поэтому оно имеет вид "dispatcher_XX", где XX - идентификатор данного модуля.

Файл глобальных констант globals.inc:

;------------------------- молитвы
P_BASE			=0		;базовый номер молитв
P_HINSTANCE		=P_BASE+0
P_MAIN_WINDOW		=P_BASE+1
P_GET_STRING		=P_BASE+2
P_GET_MAIN_WINDOW_SIZE	=P_BASE+3
P_SET_BACKGROUND_COLOR	=P_BASE+4
;------------------------- бродкасты
B_BASE			=P_BASE+100	;базовый номер бродкастов
B_START			=B_BASE+0
B_STOP			=B_BASE+1
B_MAIN_WINDOW_SIZED	=B_BASE+2

Пояснения:

  1. Возможны два варианта лептонных вызовов: молитвы и бродкасты. Молитвы применяются в случаях, когда требуется получить некий конкретный сервис (например, текстовую строку). Бродкасты нужны для того, чтобы оповещать о каких-то событиях всех, кого это может заинтересовать (например, об изменения размеров главного окна приложения). При обработке молитвы каким-либо из модулей опрос диспетчеров прекращается, и супервизор завершает свою работу. Бродкаст же передается поочередно всем диспетчерам без исключения.
  2. Видно, что идентификаторы молитв и бродкастов - это просто 32-разрядные значения в непересекающихся диапазонах. Программист должен обеспечить это требование правильной установкой базовых номеров.
  3. Здесь приведены примеры некоторых часто встречающихся молитв. P_HINSTANCE - получение дескриптора экземпляра приложения. P_MAIN_WINDOW - получение дескриптора главного окна. P_GET_STRING - получение текстовой строки из единого хранилища строк (удобно в локализуемых приложениях).
  4. Особо следует остановиться на организации обработки молитв, похожих на P_GET_MAIN_WINDOW_SIZE. Очевидно, что возвращаемое значение должно иметь тип RECT, то есть не помещается в регистр eax. Возможны два варианта решения этой проблемы. Либо при вызове этой молитвы в одном из ее параметров передается адрес памяти для приема значения RECT. В этом случае ответственность за выделение этой памяти несет модуль-клиент. Либо наоборот, память под переменную RECT выделяется модулем-сервером, и тогда клиенту возвращается ее адрес в регистре eax. И тот, и другой варианты имеют право на существование. Программист должен сделать свой выбор, создавая обработчик молитвы, исходя из ее назначения и условий использования.
  5. Бродкасты B_START и B_STOP практически необходимы в любом сколько-нибудь развитом приложении. Как следует из их имен, они предназначены для запуска приложения (т.е. инициализации модулей-серверов) и его завершения (освобожения ресурсов, занятых серверами). B_START может вызываться, например, сразу из процедуры WinMain. А B_STOP - по команде пользователя на завершение работы приложения.
  6. Для бродкаста B_START (в первую очередь для него, но иногда и для других) возникает одна интересная проблема: в какой очередности он должен обрабатываться модулями? Например, при инициализации сервера главного окна требуется, чтобы уже была доступна строка, представляющая его имя, то есть уже был проинициализирован сервер строк. В принципе, эту проблему можно было бы решить, тасуя идентификаторы в списке MODULES в файле 00_main.asm. Однако правильная последовательность не всегда очевидна, да и не обязательно должна быть одинаковой для разных бродкастов, а если она установлена таким образом, то уже останется неизменной навсегда. Поэтому следует ориентироваться на другой способ. Зависимые модули должны инициализироваться не бродкастом B_START, а тем модулем, который требует их готовности к моменту своей инициализации. Для этого можно использовать специальную молитву, например, P_START_STRINGS. На первый взгляд, описанный механизм является отступлением от лептонного стиля, который предполагает взаимную независимость модулей. Однако на практике он не вызывает проблем, так как используется в особых, крайне редких случаях, и достаточно прозрачен.

Файл главного модуля 00_main.asm:

;------------------------- список идентификаторов модулей
MODULES EQU <01,02,03,04>
;------------------------- include-файлы
include @struct.inc
include windows.inc
include globals.inc
;...
.code
;...
;------------------------- супервизор
supervisor PROC USES ebx ecx edx esi edi pray
 mov eax,pray
 ;........................ молитвы главного модуля
 @if(eax==P_HINSTANCE)
  invoke GetModuleHandleA,NULL
 @elseif(eax==P_MAIN_WINDOW)
  mov eax,main_window
 @else
  ;....................... опрос диспетчеров модулей
  mov ebx,offset(dispatchers)	;ebx - указатель в списке диспетчеров
  @while(dword ptr[ebx]!=0)
   push pray
   call dword ptr[ebx]		;вызов очередного диспетчера
   @if(pray

Пояснения:

  1. Здесь и далее применена транскрипция структурных директив MASM (.if, .while и пр.) с лидирующим символом "@" вместо точки. Обоснование, как и почему это сделано, можно прочитать в статье @struct.inc для MyCall.
  2. Список идентификаторов модулей используется макросом @dispatchers (см. выше в файле @struct.inc) на этапе компиляции для формирования списка диспетчеров, который опрашивается супервизором при каждом лептонном вызове. Программист должен, включив в проект новый модуль, дополнить список модулей его идентификатором, иначе супервизор не сможет вызвать диспетчера этого модуля.
  3. Супервизор - это очень простая процедура. Она состоит их двух независимых частей. В первой части выполняется обработка молитв (но не бродкастов!), которые по замыслу программиста должен обрабатывать главный модуль приложения. При обработке таких молитв используется только первая часть процедуры. Вторая часть - это простой цикл, сканирующий таблицу диспетчеров. Производится поочередная выборка из таблицы адресов процедур диспетчеров и передача им управления с трансляцией параметров, находящихся в регистрах esi, edi, edx, ecx.
  4. Когда супервизор обслуживает молитву, цикл опроса диспетчеров продолжается до тех пор, пока какой-нибудь из них не вернет в регистре eax ненулевое значение, что является признаком "обработано". При обслуживании бродкастов такая проверка не выполняется, поэтому супервизор обязательно перебирает всех диспетчеров.
  5. Следует иметь в виду, что в случае, когда какая-нибудь молитва не обработана ни одним из модулей приложения, супервизор, опросив всех диспетчеров, возвращает в регистре eax значение 0.
  6. Супервизор сохраняет с помощью атрибута USES директивы PROC значения всех регистров, за исключением регистра eax.

Файл модуля XX_*.asm:

;------------------------- include-файлы
include @struct.inc
include windows.inc
include globals.inc
;...
.code
;...
;------------------------- диспетчер
@dispatcher PROC USES ebx ecx edx esi edi pray
 mov eax,pray
 ;....................... обработка бродкастов
 @if(eax==B_START)
  ;...
 @elseif(eax==B_STOP)
  ;...
 ;....................... обработка молитв
 @elseif(eax==P_GET_STRING)
  mov buffer_address,@par0	;пример использования параметров молитвы
  mov buffer_size,@par1
  mov id,@par2
  ;...
  jmp ok
 ;....................... завершение диспетчера
 @endif
nok:	 ;не обработано
 xor eax,eax
ok:	 ;обработано
 ret
@dispatcher ENDP

Пояснения:

  1. Диспетчер - это процедура, которая содержится в каждом модуле, обрабатывающем лептонные вызовы - молитвы или бродкасты. Идея диспетчера очень проста: сравнение параметра pray со списком лептонных вызовов, обрабатываемых данным модулем. Если полученный лептонный вызов обрабатывается, то после его обработки диспетчер возвращает управление супервизору с ненулевым (полученным в результате обработки) значением в регистре eax (выполняется выход на метку ok:). В противном случае регистр eax сбрасывается в 0 (выполняется выход на метку nok:). Что касается бродкастов, то все равно, на какую метку передается управление по завершении их обработки.
  2. Имя диспетчера, поскольку оно глобально в пределах исполняемого модуля, должно быть уникальным. А диспетчеров у нас - по одному на каждый модуль. Во избежание конфликтов здесь применен следующий прием. Имя @dispatcher на самом деле является вызовом макроса, описанного в файле @struct.inc (см.выше). Этот макрос формирует действительное имя процедуры вида dispatcher_XX, где XX - идентификатор модуля.
  3. Входными значениями для диспетчера являются передаваемый через стек идентификатор лептонного вызова (pray) и до четырех параметров, передаваемых через регистры: @par0 (он же регистр esi), @par1 (edi), @par2 (edx), @par3 (ecx). Эквиваленты параметров определены в файле @struct.inc.
  4. Использовать в качестве параметров действительные имена регистров или их эквиваленты - зависит от предпочтений программиста. Применение эквивалентов, освобождая программиста от необходимости помнить то, в каком регистре передается какой параметр, одновременно может явиться источником тяжелых ошибок, если программист вздумает вдруг воспользоваться эквивалентами регистров в качестве входных параметров диспетчера после того, как что-нибудь с этими регистрами уже поделает.
  5. Можно придумать много различных вариантов организации диспетчера, оптимизирующих как использование памяти, так и время выполнения (если это имеет смысл). Здесь показан простейший вариант, основанный на применении директив @if-@elseif-@endif.
  6. Показанное здесь разбиение процедуры диспетчера на части (обработка бродкастов и обработка молитв) условно. На самом деле очередность выполнения сравнений, конечно же, смысла не имеет.
  7. Диспетчер, также как и супервизор, сохраняет значения всех регистров, за исключением регистра eax. Это помогает уберечься от труднообнаруживаемых ошибок, связанных со случайным изменением содержимого регистров в процессе опроса диспетчеров.

В заключение несколько дополнительных замечаний:

  • легко видеть, что деление лептонных вызовов на молитвы и бродкасты весьма условно. Можно обойтись одними молитвами, если в отношении тех молитв, которые должны выполнять роль бродкастов, аккуратно выполнять правило: каждый диспетчер должен возвращать нулевое значение;
  • использование нулевого значения регистра eax в качестве признака "не обработано" может вызвать возражения. Как в таком случае отличить случай "не обработано" от случая, когда результат обработки равен 0? Однако на самом деле это никакая не проблема. Во-первых, именно так работают большинство функций API win32. Во-вторых, если есть необходимость, можно придумать множество разных средств для разрешения этой ситуации. Например, ввести молитву P_GET_LAST_ERROR, по принципу использования аналогичную функции GetLastError API win32. Молитва должна возвращать значение, соответствующее последней ошибочной ситуации. А уж установить это значение в случае, если супервизор безуспешно просканировал весь список диспетчеров - не проблема;
  • предложенная технология может служить основой для создания библиотек молитв. Во всяком случае наш опыт показал, что, раз встав на эту скользкую дорожку, сойти с нее оказывается очень трудно. И вот уже кочуют из проекта в проект модули, внутреннее устройство которых давно забыто, а для встраивания в приложение вполне хватает интерфейса, представленного набором молитв;
  • небольшая модификация лептонной идеи легко расширяет ее для реализации в проектах, использующих собственные dll-библиотеки. При этом появляются молитвы вроде P_LOAD_DLL и P_FREE_DLL, назначение которых очевидно, но имеется одна особенность. Следует предусмотреть несложный механизм, включающий диспетчеров модулей, загруженных в виде dll-библиотек, в список диспетчеров, сканируемый супервизором. Сопутствующие мелкие проблемы (вроде инициализации загружаемых dll-библиотек, которым не довелось присутствовать при прохождении бродкаста B_START) вполне решаемы.

2002-2013 (c) wasm.ru