Статьи по Assembler

       

Лептонный стиль программирования - реализация


Это вторая часть статьи. Постановка задачи - в первой части.

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

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

Ниже приведены фрагменты кода, включаемые




Статьи по Assembler

       

Ниже приведены фрагменты кода, включаемые


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

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

;------------------------- эквиваленты параметров молитвы @par0 EQU esi @par1 EQU edi @par2 EQU edx @par3 EQU ecx ;------------------------- макрос молитвы @pray MACRO pray,par0,par1,par2,par3 IFNB <par0> mov esi,par0 ENDIF IFNB <par1> mov edi,par1 ENDIF IFNB <par2> mov edx,par2 ENDIF IFNB <par3> 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 <dispatcher_>,@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 <dispatcher_>,@module ;сконструировать имя диспетчера ENDIF

Пояснения:


  • Эквиваленты параметров молитвы используются внутри диспетчеров (см.ниже в файле XX_*.asm). Они нужны для того, чтобы у программиста не было необходимости помнить, какой параметр передается в каком регистре. Символ "@" напоминает программисту о том, что это регистры, а не адреса памяти.
  • Макрос молитвы используется для формирования лептонных вызовов из программы - молитв и бродкастов, например: @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


  • Макрос формирования списка диспетчеров используется только один раз - при компиляции главного модуля. На основании списка идентификаторов модулей MODULES (см. ниже в файле 00_main.asm) он создает в константном сегменте приложения таблицу адресов процедур диспетчеров всех модулей. Таблица завершается нулевым значением. При каждом лептонном вызове супервизор поочередно передает управление по адресам из этой таблицы, вызывая, таким образом, диспетчеры модулей.
  • Прототип супервизора обеспечивает связывание лептонных вызовов из модулей с процедурой супервизора, описанной в главном модуле 00_main.asm.
  • Идентификатор модуля представляет собой два первых символа имени asm-файла. Можно придумать и другие варианты, но нам показался привлекательным этот. Он удобен еще и тем, что в панели Workspace рабочей среды MS Developer Studio именованные таким образом файлы располагаются в строгом порядке, так как происходит их сортировка по имени.
  • В зависимости от того, какой модуль приложения компилируется - главный или обычный, - в нем либо создается список диспетчеров, либо конструируется имя диспетчера. Имя диспетчера, будучи глобальным, должно быть уникально в пределах проекта. Поэтому оно имеет вид "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



Пояснения:


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




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

;------------------------- список идентификаторов модулей MODULES EQU

;------------------------- 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<B_BASE) ;если это молитва, а не бродкаст @if(eax) ;и если вызов обработан @break ;то завершить опрос диспетчеров @endif @endif add ebx,4 ;перейти к следующему диспетчеру @endw @endif ret supervisor ENDP

Пояснения:


  • Здесь и далее применена транскрипция структурных директив MASM (.if, .while и пр.) с лидирующим символом "@" вместо точки. Обоснование, как и почему это сделано, можно прочитать в статье @struct.inc для MyCall.
  • Список идентификаторов модулей используется макросом @dispatchers (см. выше в файле @struct.inc) на этапе компиляции для формирования списка диспетчеров, который опрашивается супервизором при каждом лептонном вызове. Программист должен, включив в проект новый модуль, дополнить список модулей его идентификатором, иначе супервизор не сможет вызвать диспетчера этого модуля.
  • Супервизор - это очень простая процедура. Она состоит их двух независимых частей. В первой части выполняется обработка молитв (но не бродкастов!), которые по замыслу программиста должен обрабатывать главный модуль приложения. При обработке таких молитв используется только первая часть процедуры. Вторая часть - это простой цикл, сканирующий таблицу диспетчеров. Производится поочередная выборка из таблицы адресов процедур диспетчеров и передача им управления с трансляцией параметров, находящихся в регистрах esi, edi, edx, ecx.
  • Когда супервизор обслуживает молитву, цикл опроса диспетчеров продолжается до тех пор, пока какой-нибудь из них не вернет в регистре eax ненулевое значение, что является признаком "обработано". При обслуживании бродкастов такая проверка не выполняется, поэтому супервизор обязательно перебирает всех диспетчеров.
  • Следует иметь в виду, что в случае, когда какая-нибудь молитва не обработана ни одним из модулей приложения, супервизор, опросив всех диспетчеров, возвращает в регистре eax значение 0.
  • Супервизор сохраняет с помощью атрибута 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

Пояснения:


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




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


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



Содержание раздела






Содержание раздела