Из журнала Scenergy #1, Новгород, 1999 (C) Flying/DR О пользе макросов или TASM v4.12 - rulez! 0. Вступление Немного странное название для статьи, не находите? :) Однако содержание полностью соответствует названию. Написать эту статью меня побудили разговоры в FIDO'шной эхе ZX.SPECTRUM, из которых я в частности узнал одну удивительную для меня вещь - оказывается большинство народа попросту не знает, что такое макросы и условная компиляция и с чем вообще все это едят. Другой движущей силой к написанию этой статьи стало горячее желание еще раз сказать: "Какое счастье, что на свете есть такой замечательный ассемблер как TASM v4.12 by RST7/CBS!". Ну вот, сказал, теперь можно спать спокойно. :) Другими словами отчасти данная статья призвана показать, чем же все-таки TASM 4 выделяется среди остальных ассемблеров, что в нем есть такого, чего нет, к сожалению, больше нигде, и из-за чего я выбрал именно его, после того как перепробовал около десятка ассемблеров. А отличают его именно 2 упомянутые выше вещи - наличие макросов и условной компиляции. Точнее есть еще одна вещь - клавиатурные макросы, но к теме данной статьи она не относится, хотя тоже является немерянным rulez'ом :) Сразу скажу: эта статья - не агитация всех кодеров "переходите на TASM!" (ну может только в некоторой степени), а скорее призыв к авторам ассемблеров - почему такие прекрасные вещи до сих пор отсутствуют во всех ассемблерах? Однако вернемся к основной теме. Сначала, для тех, кто вообще не знаком с понятиями "макросы" и "условная компиляция", я попытаюсь в общих чертах объяснить, что это такое. 1. Что такое макросы? Итак, макросом называется конструкция следующего вида: DEFMAC <Имя макроса> <Тело макроса> ENDMAC При этом <Имя макроса> - любая метка, а тело макроса - любой кусок кода в стандартном синтаксисе данного ассемблера. Пример макроса: DEFMAC CLRSCR LD HL,#4000 LD DE,#4001 LD BC,#17FF LD (HL),L LDIR ENDMAC Использование макроса в коде осуществляется путем указания в теле программы имени макроса: HALT XOR A OUT (#FE),A CLRSCR ;Использование макроса При компиляции будет сгенерирован следующий код: HALT XOR A OUT (#FE),A LD HL,#4000 ;Этот кусок LD DE,#4001 ;кода вставлен LD BC,#17FF ;вместо имени макроса. LD (HL),L ; LDIR ; Макросы могут иметь параметры. В синтаксисе TASM'а параметры именуются как \0, \1, \2, ... \9. Т.е. каждому макросу можно передать до 10 параметров. Посмотрим, как это используется на примере той же очистки. Однако теперь это будет уже очистка не только экрана, а любого участка памяти: DEFMAC CLEAR LD HL,\0 LD DE,\1 LD BC,\2 LD (HL),0 LDIR ENDMAC Соответственно вызов данного макроса: HALT XOR A OUT (#FE),A CLEAR #4000,#4001,#17FF Генерируемый код естественно идентичен. Но это выглядит не совсем красиво. Поэтому лучше немного переделать этот макрос: DEFMAC CLEAR LD HL,\0 LD DE,0+((\0)+1) LD BC,0+((\1)-1) LD (HL),0 LDIR ENDMAC Соответственно измениться и вызов: CLEAR #4000,#1800 Надеюсь, понятно? Единственный вопрос может быть о выражениях для DE и BC. Объясню: параметры, передаваемые в макрос, заключены в скобки, для того чтобы можно было передавать в качестве параметров не только константы, но и выражения. А "0+" в начале нужен для того, чтобы показать ассемблеру, что это не команда LD DE,(nn), а LD DE,nn. Естественно описанный макрос примитивен, но зато понятен и дает представление о том, что такое макросы. 2. Что такое условная компиляция? Теперь об условной компиляции. В синтаксисе TASM'а условная компиляция выглядит следующим образом: .IF <выражение> <1-й кусок кода> .ELSE <2-й кусок кода> .ENDIF <выражение> - любое выражение понимаемое ассемблером. Так вот, если значение <выражения> на этапе компиляции будет равно 0, то будет откомпилирован <1-й кусок кода>, если же нет, то будет откомпилирован <2-й кусок кода>. Зачем это нужно? Наиболее часто это используется для того чтобы иметь возможность генерировать различный код из одного и того же исходника без его изменения. Типичный пример - у Вас есть некий проект, который Вы разрабатываете. Пока проект находится в стадии разработки и отладки, в нем обычно находятся куски отладочного кода (различные ловушки, заглушки, проверки нажатия SPACE для выхода, какие-нибудь счетчики прерываний и прочее). Естественно, что весь этот отладочный код придется вычищать на стадии сборки готового проекта. Как правило, это выливается в ползанье по исходнику с бормотанием себе под нос "Ну ведь где-то здесь была эта проверка... Куда она подевалась... ?" :) А потом, если вдруг снова потребуется вернуть отладочный код на место (что тоже случается часто, ведь готовый проект солидных размеров редко собирается с первого раза), вся эта процедура повторяется заново, а потом еще и еще раз... Одним словом - грустное зрелище... А теперь рассмотрим ту же ситуацию, но с использованием условной компиляции: 1) В начале исходника пишется строчка: DEBUG EQU 0 ;0 - выполнение условия 2) Весь отладочный код пишется как: .IF DEBUG <отладочный код> .ENDIF Теперь, для того чтобы скомпилировать отладочную версию программы нам необходимо установить DEBUG=0,а чтобы откомпилировать версию без отладочного кода - установить DEBUG=1. И все! Я думаю, Вы согласитесь с моим мнением, что так работать намного удобнее. 3. Примеры использования Рассмотрим некоторые другие возможности применения макросов и условной компиляции в Ваших программах. Т.к. учитель из меня никудышный и примеры на ходу я придумывать не могу - буду брать только реальные примеры использования из моих программ. 3.1 Распределение памяти Пожалуй, это применение макросов нашло самое широкое распространение в моих программах. Попросту говоря, все мои программы, написанные за последний год, работают именно с таким способом распределения памяти. Причем я нахожу это очень удобным. Здесь я вкратце опишу методику и приведу примеры макросов, а сами исходники этого модуля Вы найдете в приложении. Основная идея состоит в том, чтобы облегчить программисту задачу выделения динамической памяти и автоматизировать контроль за ее переполнением. Я думаю, любой программист сталкивался с такими ситуациями, когда у него глючила программа из-за того, что он выделил под таблицу или генерируемую процедуру на 1 байт меньше памяти, чем надо. Также наверняка все сталкивались с необходимостью вручную пересчитать адреса расположения в памяти десятка табличек, если первая из них вдруг изменила размер. Так вот, использование макросов позволяет полностью избавить программиста от всех этих проблем! Для начала надо прийти к соглашению по поводу организации памяти. Я опишу, как это сделано у меня, а вообще-то этот метод можно подогнать под любую схему распределения памяти. Итак, память условно поделена на следующие участки: #6000-#7FFF - Медленная память. Используется для хранения таблиц. #8000-#xxxx - Код программы. #xxxx-#BDFF - Быстрая память. Используется для хранения генерируемых процедур, а также тех таблиц, которые не влезли в slow memory. #BE00-#BFFF - Таблица прерываний и все что связано с обработчиком прерываний. #C000-#FFFF - Верхняя память. Используется для хранения больших таблиц и/или процедур. Используя такое распределение памяти, мы дополнительно обеспечиваем большую вероятность нормальной работы программ на машинах с медленной памятью. Ну да ладно, это к делу не относится. Как правило, требуется 2 типа выделяемой памяти - с произвольным адресом и с адресом, выровненным по границе 256 байт (т.е. с маской адреса #XX00). Таким образом для решения задачи выделения памяти нам необходимо 6 макросов (3 типа памяти * 2 типа выделения): Для выделения памяти с произвольным адресом: GET_SLOW_MEMORY - в slow memory GET_FAST_MEMORY - в fast memory GET_UPPER_MEMORY - в upper memory Для выделения памяти с адресом, выровненным по границе 256 байт: GET_SLOW_MEM_XX00 - в slow memory GET_FAST_MEM_XX00 - в fast memory GET_UPPER_MEM_XX00 - в upper memory Общий формат вызова: GET_nnn_MEMORY Variable,Mem_Size GET_nnn_MEM_XX00 Variable,Mem_Size Пример: GET_SLOW_MEMORY PROC_1 , $120 GET_SLOW_MEMORY PROC_2 , $A0 GET_SLOW_MEM_XX00 TABLE_1, $200 GET_SLOW_MEM_XX00 TABLE_2, $100 В результате после компиляции получаем: PROC_1=#6000 PROC_2=#6120 TABLE_1=#6200 ;А не #61C0! TABLE_2=#6400 Соответственно если размер любой из выделенных областей памяти изменится - все адреса будут пересчитаны при компиляции автоматически, причем с сохранением всех необходимых выравниваний по границам 256 байт! Ну не сказка ли? :) Посмотрим как все это реализуется: ;SLOW_MEMORY - переменная, в которой ;хранится указатель на первый свободный ;байт. ;Изначально SLOW_MEMORY=#6000. ;Параметры макросов: ;\0 - Имя создаваемой переменной ;\1 - Размер выделяемой памяти DEFMAC GET_SLOW_MEMORY ;Создаем переменную \0 EQU 0 ;Присваиваем ей текущее значение указателя ;свободной памяти \0=SLOW_MEMORY ;Корректируем значение указателя в ;соответствии с размером выделенного блока ;памяти. SLOW_MEMORY=SLOW_MEMORY+(\1) ENDMAC DEFMAC GET_SLOW_MEM_XX00 ;Создаем переменную \0 EQU 0 ;Если младший байт текущего адреса ;указателя не равен нулю... .IF SLOW_MEMORY&#FF-0 .ELSE ;...То переставляем указатель на следующий ;адрес памяти, выровненный по границе ;256 байт. SLOW_MEMORY=((SLOW_MEMORY/#100)+1)*#100 .ENDIF ;Присваиваем переменной текущее значение ;указателя свободной памяти. \0=SLOW_MEMORY ;Корректируем значение указателя в ;соответствии с размером выделенного блока ;памяти. SLOW_MEMORY=SLOW_MEMORY+(\1) ENDMAC Макросы для fast и upper memory работают аналогично. Кроме того, в файле MEMORY.A, находящемся в приложении Вы можете посмотреть макрос DISPLAY_INFO, который помогает отследить переполнение выделенной памяти. Из-за его большого размера я его здесь приводить не буду. 3.2 Вычисления Макросы здорово облегчают задачу вычисления какого-либо выражения в Вашей программе. Часто бывает необходимо задать какой-нибудь адрес в виде метки с тем, чтобы в дальнейшем его использовать. Если подобный адрес 1 - его можно задать вручную. А вот если их, например, десяток, да еще их приходится время от времени менять - поневоле задумаешься. Особенно часто это бывает с экранными адресами. Для этого тоже можно написать достаточно простые макросы, которые, тем не менее, значительно облегчают жизнь: ;Вычисление экранного адреса. ;Параметры макроса: ;\0 - Имя создаваемой переменной ;\1 - X координата (в знакоместах) ;\2 - Y координата (в пикселях) DEFMAC GET_SCRADR \0 EQU #4000 \0=\0+((\2)&#C0*32)+((\2)&7*#100)+((\2)&*4)+((\1)F) ENDMAC ;Вычисление адреса в атрибутах. ;Параметры макроса: ;\0 - Имя создаваемой переменной ;\1 - X координата (в знакоместах) ;\2 - Y координата (в знакоместах) DEFMAC GET_ATTRADR \0 EQU #5800 \0=\0+((\2)*32)+((\1)F) ENDMAC Кроме того, при желании эти макросы элементарно переделываются на то чтобы сразу записывать в адрес память - это очень удобно для составления различных таблиц адресов. Например: DEFMAC DEFW_ATTRADR DEFW #5800+((\2)*32)+((\1)F) ENDMAC ADR_TABLE DEFW_ATTRADR 10,5 DEFW_ATTRADR 12,6 DEFW_ATTRADR 14,7 DEFW_ATTRADR 16,8 DEFW_ATTRADR 18,9 Это, конечно, только примеры. Реально на макросах можно реализовать гораздо более сложные вещи. Например, в Scenergy main menu у меня на макросах написан расчет таблицы указателей на спрайты, составляющие анимацию пунктов меню. При этом задаются только количество спрайтов и их размеры, а макросы выполняют пересчет адресов, причем даже с учетом перехода на следующую страницу (т.к. под спрайты отводится 2 страницы). Но здесь я этот макрос приводить не буду из-за его размера (2 экрана в TASM'е :) ). 3.3 Структуры данных С помощью макросов легко реализуется работа со структурами данных. Наверняка каждому из Вас при написании программы приходилось сталкиваться с тем, что данные о каком-то объекте имеют несколько полей, причем эти поля разного типа. Например, данные о пункте меню: [byte] - X координата [byte] - Y координата [word] - Указатель на следующий пункт [word] - Указатель на предыдущий пункт [byte] - Длина [byte] - Hotkey [byte] - Тип пункта меню [byte] - Дополнительные флаги [word] - Указатель на процедуру-обработчик [word] - Указатель на параметр [word] - Указатель на комментарий [data] - Название пункта меню Кстати, эта структура данных реально описывает пункт меню в Scenergy Setup'е. Каждый элемент структуры данных характеризуется 2-мя вещами: - своим типом - смещением относительно начала структуры Как правило, в программах эти смещения задаются константами. Соответственно, если, не дай Бог, какое-то поле необходимо будет удалить или изменить его тип (а значит и размер) - все смещения также изменятся! А, следовательно, Вам предстоит в лучшем случае пересчитать заново все смещения и набить их в соответствующие EQUS. Либо, если Вас угораздило писать все эти смещения внутри программы в виде чисел - Вам предстоит провести долгое время в поисках всех этих смещений, да еще потом проверить - а не забыл ли чего? Одним словом - удовольствие на грани мазохизма :) Чем же здесь могут помочь макросы? А вот чем. Пишем маленький такой макрос: ___ORG EQU 0 DEFMAC _SZ \0 EQU ___ORG ___ORG = ___ORG+(\1) ENDMAC И задаем все смещения в следующем виде: ___ORG = 0 _SZ _MI.X ,1 _SZ _MI.Y ,1 _SZ _MI.NEXT ,2 _SZ _MI.PREV ,2 _SZ _MI.LEN ,1 _SZ _MI.HOTKEY ,1 _SZ _MI.TYPE ,1 _SZ _MI.FLAGS ,1 _SZ _MI.HANDLER ,2 _SZ _MI.PARAM ,2 _SZ _MI.COMMENT ,2 _SZ _MI.TITLE ,0 Что нам это дает? 1) Отсутствует необходимость вручную считать все смещения, отсутствует вероятность сделать при этом ошибку. 2) Получается более наглядно. 3) При любом изменении все пересчеты смещений будут сделаны автоматически! И еще одно. А именно способ задания этих структур данных. Обычно это выглядит примерно так (для структуры пункта меню): MI_1 ;Предыдущий пункт меню MI_2 DEFB 3,5 DEFW MI_3,MI_1 DEFB 10 .ENDIF .IF USE_SMULT8 <процедура знакового умножения D*E=HL> .ENDIF .IF USE_MULT16 <процедура умножения HL*DE=HLDE> .ENDIF .IF USE_DIV8 <процедура деления H/L=H.L> .ENDIF .IF USE_DIV16 <процедура деления HL/DE=HL.DE> .ENDIF Все это записывается в виде отдельного модуля, например MATH.A. Теперь если в DEFB "I",2,5 DEFW HANDLER,PARAM,0 DEFB "MENU ITEM",0 MI_3 ;Следующий пункт меню С использованием же макросов это может выглядеть следующим образом: MI.X = 0 MI.Y = 1 MI.LEN = 12 MI.TYPE = MIT_NORMAL MI.FLAGS = MIF_ACTIVE+MIF_SELECTED MI.HANDLER = CONTROLS MI.PARAM = 0 MI.COMMENT = C011 MENUITEMDATA "~CONTROLS..." Это реальный кусок исходника от Scenergy Setup'а. Кроме того, использование макросов позволило в данном случае избавиться от утомительной операции объединения пунктов меню в двусвязный список. Однако здесь я эти макросы также не привожу в силу их громоздкости. Лично мне такой способ задания данных нравится больше, хотя, как оказалось, мое мнение разделяют далеко не все. Например, всеми уважаемый Oldman раскритиковал этот способ в пух и прах :) Конечно, это дело вкуса, но люди имеют право знать об альтернативах стандартной работе со структурами. 3.4 Расширение синтаксиса В принципе работа со структурами данных реализованная на макросах тоже является расширением синтаксиса, но я вынес ее в отдельную часть. Здесь мы поговорим о расширении синтаксиса самого ассемблера. Конечно, лучше всех это может рассказать автор TASM'а - RST7. Стоит вспомнить хотя бы его библиотеку 6502.A, которая не просто расширяет синтаксис языка, а, по сути, превращает TASM в компилятор для совершенно другого процессора! Но до таких высот мы подниматься не будем, начнем с чего-нибудь более простого. Кстати, я специально не касался такой возможности макросов в TASM'е как разбор строки параметров т.к. это вещь не настолько тривиальна, чтобы объяснять всем, что это такое. Те, кому это будет нужно - сами прочитают описание TASM'а и поймут. Так вот, насчет синтаксиса. Для начала посмотрим, как можно реализовать STROM'овские макрокоманды, например LD HL,DE: DEFMAC LD_HL_DE LD H,D LD L,E ENDMAC Работу с несколькими параметрами в LD тоже можно сделать с помощью макросов, но это не совсем тривиальная задача. Кому это будет надо - сделают сами. Так же с помощью макросов можно сделать более читабельными отдельные участки Вашего кода. Я уже приводил пример с очисткой, подобных вещей можно придумать множество. Кроме того, используя условную компиляцию можно вносить туда зачатки оптимизации. Например: DEFMAC LD_A .IF \0 XOR A .ELSE LD A,0+(\0) .ENDIF ENDMAC Надеюсь понятно? Если параметр макроса равен нулю - то вместо LD A,n будет XOR A. Это бывает нужно, если A присваивается не константа, а выражение или метка и заранее неизвестно каково будет ее значение. Это только простейший пример чтобы Вы уловили основную идею. 4. Увеличение гибкости кода с помощью условной компиляции. Что я подразумеваю под словами, вынесенными в заголовок? А очень простую вещь - минимизацию исправлений готовых исходников для придания им вида приспособленного для использования их в других программах. Рассмотрим конкретный пример. Допустим, у Вас есть некий кусок кода, который почти без изменений переходит из одной Вашей программы в другую. Это могут быть разные вещи. Например, у меня это группа модулей обеспечивающих различные низкоуровневые функции для создания эффектов в chunk'ах. Что обычно делает программист, если ему хочется использовать кусок своего исходника в другой своей программе? Обычно он сначала разбирается в своем предыдущем исходнике, пытаясь понять, какие еще процедуры и таблицы необходимы для того куска кода, который он хочет перенести, какие есть тонкости в его использовании. Надо также контролировать - а не скопировал ли я чего лишнего? Использование условной компиляции помогает избежать всех этих проблем и сделает Ваши исходники переносимыми между всеми Вашими программами. Рассмотрим типичный пример - процедуры рализующие математические вычисления. Как правило, у каждого кодера есть набор подобных функций на все случаи жизни - умножения, деления и т.п. Причем зачастую эти функции кодер пишет не сам, а берет готовые. Т.е. повторить их "из головы" он вряд ли сможет. И вот, допустим, понадобилась ему процедура умножения. Что он делает? Как правило, начинает вспоминать: а в какой программе я использовал эту процедуру последний раз? Потом начинаются поиски исходников этой программы. Потом - поиск этой процедуры в исходниках. И так каждый раз. Можно, конечно пойти по другому пути - хранить все подобные процедуры в виде отдельного исходника и брать куски кода из него. Можно хранить на отдельном диске много маленьких исходничков. А можно пойти по другому пути: Все подобные процедуры объединяются в один модуль. Но не просто так, а с дополнительной обвязкой: - Вначале стоит определение набора условий: USE_MULT8 EQU 1 ;Умножение D*E=HL, беззнаковое USE_SMULT8 EQU 1 ;Умножение D*E=HL, знаковое USE_MULT16 EQU 1 ;Умножение HL*DE=HLDE, беззнаковое USE_DIV8 EQU 1 ;Деление H/L=H.L, беззнаковое USE_DIV16 EQU 1 ;Деление HL/DE=HL.DE, беззнаковое В дальнейшем, используя их, мы сможем выделять необходимые нам процедуры. - Затем идет описание самих процедур: .IF USE_MULT8 <процедура умножения D*E=HL> .ENDIF .IF USE_SMULTB <процедура знакового умножения D*E=HL> .ENDIF .IF USE_MULT16 <процедура умножения HL*DE=HLDE> .ENDIF .IF USE_DIV8 <процедура деления H/L=H.L> .ENDIF .IF USE_DIV16 <процедура деления HL/DE=HL.DE> .ENDIF Все это записывается в виде отдельного модуля, например MATH.A. Теперь если в какой-либо программе Вам потребуется любая процедура умножения/деления из тех, что есть в модуле, Вам необходимо будет сделать только 2 вещи: - Добавить в исходник строчку: USE_<код процедуры>=0, например USE_MULT8=0 - Добавить в конец исходника строчку: .INCLUDE MATH Все! Вы можете спокойно использовать эту процедуру ни о чем не заботясь! При компиляции этот модуль будет прилинкован, но в коде будет только одна процедура - та, которую Вы выбрали. И все это без какого-либо вмешательства в код модуля! Это удобно и еще по одной причине: например у меня подобных модулей которые линкуются к моим программам без вмешательства в их код наберется около десятка. И при этом почти во всех используются различные математические процедуры. В любом другом случае я получал бы при компиляции несколько копий одной и той же процедуры в памяти, а в данном случае я всегда буду иметь в памяти только тот код, который реально используется при работе программы. Причем мне не приходится заботиться о его отладке или подгонке под конкретную задачу - все это делается автоматически на этапе компиляции! Мне остается только управлять генерацией нужного мне кода путем переприсваивания нужных значений меткам, отвечающим за настройку кода! Кстати, все проблемы связанные с возможностью наложения участков памяти, выделенной разными модулями, для меня также не существуют из-за описанной выше макробиблиотеки отвечающей за выделение памяти. В результате для того чтобы написать очередной эффект мне необходимо написать только процедуры отвечающие непосредственно за алгоритм самого эффекта даже не задумываясь обо всех низкоуровневых процедурах делающих всю "черную" работу. Не знаю, может кому-то это и покажется ненужным, неправильным, но мне это нравится :) 5. Заключение Вот примерно так протекает жизнь кодера использующего в своих программах макросы и условную компиляцию :) Естественно, что здесь я не рассказал всего, это слишком мощные инструменты в умелых руках, и все возможности их использования нельзя охватить в рамках одной статьи. Но даже то, что было рассказано здесь, imho дает некоторое представление о том, зачем все это нужно. Надеюсь, что данная статья многих заставит задуматься. Ведь как показывает практика - народ не использует всех этих наворотов и считает их ненужными просто потому что не знает, что же это такое и как это использовать. Теперь, после этой статьи Вы знаете о макросах и условной компиляции достаточно для их использования. Может кто-нибудь даже пересядет ради них с ALASM/STORM на TASM, чему я, как большой фанат TASM'а, буду очень рад :) Если вдруг случится чудо и эту статью прочитает RST7/CBS, то я хочу чтобы он знал, что по моему (и не только по моему) мнению TASM - лучший ассемблер на Speccy. Я очень надеюсь на выпуск TASM v5.0 и заранее готов купить его! Собственно, от новой версии я жду только 3-х вещей: - вложенные макросы - вложенная условная компиляция - таблица меток размером этак 64кб После этого TASM станет еще более полным rulez'ом, чем он есть сейчас! Да, кстати, не могу упомянуть об еще одной вещи. Если в начале программы задать следующие EQUS: YES EQU 0 NO EQU 1 то жить станет намного легче т.к. выражение USE_MULT8=YES читается несравненно легче, нежели USE_MULT8=0.