1. Основные определения
2. Позиционные параметры
3. Соотношение понятий "макрос" и
"подпрограмма"
4. Структуры данных и логика работы
макропроцессора
5. Ключевые параметры
6. Конкатенация параметра с текстом
7. Генерация уникальных меток
8. Условная макрогенерация
9. Препроцессор языка C
Макропроцессор (МП) - системная программа преобразования текста в соответствии с имеющимися в нём инструкциями. Основное назначение - повышение уровня программирования на основе подстановки текста. Впервые стали применяться для построения ассемблерных программ. Совмещение ассемблера с МП называется макроассемблером. Макропрцессоры также могут встраиваться в ЯВУ. В этом случае МП называется препроцессором.
В качестве иллюстрации мы в этом разделе будем рассматривать обработку текстов ассемблерных программ для учебной ЦВМ.
Центральное понятие МП-обработки - макровызов (МВ). Макровызов - это краткое символическое обозначение некоторого текста, который он представляет в программе. Этот "некоторый текст" оформляется в виде так называемого макроопределения (МО). Задача МП состоит в подстановке вместо МВ текстов, которые формируются на основе соответствующих МО. Процесс замены МВ текстом на основе МО называется макрорасширением (МР). Так же называется и текст-результат такой замены.
Определим возможный синтаксис записи рассмотренных конструкций и приведём примеры.
Синтаксис МВ:
[<метка>] <имя_МВ> [<список_фактических_параметров>]
Синтаксис МО:
[<имя_МВ>] macro [<список_формальных_параметров>] <Модельные_операторы> mend
Пример.
Вход МП | Выход МП |
prg1 start 0 sqr macro lda data mul data sta data mend ... sqr ... sqr ... end prg1 |
prg1 start 0 ... lda data mul data sta data ... lda data mul data sta data ... end prg1 |
Точки представляют ту часть входа, которая не подвергается преобразованию МП. МО примера не содержит параметров - о них речь впереди. Мы видим, что каждый МВ sqr заменяется соответствующим МР, которое строится на основе модельных операторов МО. Фактически в данном случае мы видим пример простой подстановки. Если на входе МП мы имеем макропрограмму с МО и МВ, то на его выходе мы уже получаем чистую ассемблерную программу, которую, в свою очередь, можно подать на вход обычного ассемблера.
Несколько слов о месторасположении МО. Удобнее всего их размещать в начале макропрограммы. Они также могут размещаться в специальных системных библиотеках МО. Такие МО называют системными. В том случае, если МО из макропрограммы конфликтует с системным МО, то обычно для МР используется МО из макропрограммы.
В литературе наряду с введёнными терминами используют их синонимы: МО - макрос, МВ - макрокоманда, МР - макроподстановка.
Параметры позволяют модифицировать МО. Приведём пример.
Вход МП | Выход МП |
prg2 start 0 movw macro &source,&dest lda &source sta &dest mend ... move da,db ... move db,data ... end prg2 |
prg2 start 0 ... lda da sta db ... lda db sta data ... end prg2 |
Синтаксис параметра: &<имя>
В этой связи символ "&" приобретает в МП статус системного.
В ходе МР модельные операторы из МО модифицируются подстановкой вместо формальных параметров их фактических значений, то есть тех строк, которые составляют фактические параметры или аргументы. Соответствие формальных параметров и аргументов - позиционное. Аргумент может быть опущен, но запятая, отделяюшая его от следующего аргумента, если он присутствует, должна быть проставлена. В этом случае соответствующий формальный параметр замещается пустой строкой.
Из приведенных примеров уже ясно, что техника МП-обработки напоминает работу с подпрограммами. У этих двух подходов много общего, но всегда следует помнить, что подпрограмма - это средство периода выполнения программы, а макрос - периода макрогенерации (ПМГ). Так как оба подхода обечпечивают повышение уровня программирования, то возникает вопрос о их взаимозаменяемости. На этот вопрос следует ответить утвердительно - подпрограмму можно заменить на макрос и наоборот. И сразу же возникает ещё один вопрос: "В каких случаях следует предпочесть тот или иной подход"? Чтобы на него ответить выполним кое-какие расчёты. Сначала выполним сравнение для конкретного примера, а потом обобщим полученные результаты. Критерии сравнения - объём кода и время выполнения программы.
Макрос | Подпрограмма | |
Вызов | sqr data |
jsub sqr word data |
Определение | sqr macro &d lda &d mul &d sta &d macro |
sqr rmo l,x ldx 0,x lda 0,x ; mul 0,x ; - Vb sta 0,x ; rmo l,a add c3 rmo a,l rsub c3 word 3 |
Расширение | lda data mul data sta data |
Динамическое (т.е. во времени) |
Из примера мы видим, что три команды - lda, mul и sta - выполняют содержательную обработку в обоих вариантах. Введём общие обозначения V и T соответствено для объёма кода и времени его выполнения. Тогда по отдельным категориям кода можно ввести следующие обозначения:
Тогда для определения Vm, Tm и Vs, Ts в расчёте на N вызовов можно использовать следующие формулы:
Макрос | Подпрограмма |
Vm = N * Vb | Vs = N * Vc + Vb + Vj |
Tm = N * Tb | Ts = N * (Tb + Tc + Tj) |
Анализ этих формул позволяет сделать следующие выводы:
Общий вывод таков: макрос имеет преимущество, когда затраты на вызов и связывание превышают затраты на реализацию содержательной части кода.
Будет интересно выполнить сравнение вариантов
для нашего примера. Полагая, что выполнение одной
команды занимает одну единицу условного времени,
получим: Vb = 9, Tb = 3, Vc = 6, Tc = 1, Vj = 19, Tj = 6,
Vm = N * 9, Vs = N * 6 + 28,
Tm = N * 3, Ts = N * 10.
Выводы:
Однако, если предположить, что при тех же параметрах вызова и связывания мы имеем дело с в десять раз более крупной программной единицей, для которой Vb = 90 и Tb = 30, то выигрыш макроса по времени выполнения составит только 23%, а по объёму кода выигрыш будет только при N = 1 и уже при N = 10 подпрограмма будет иметь впечатляюшее преимущество по объёму кода - более чем в 5 раз!
Мы видим, что у каждого подхода есть своя ниша применения. Кроме того надо отметить, что макросы незаменимы при необходимости генерировать ПО в многочисленных вариантах. В качестве примера можно привести обычную практику составления двух вариантов программы - отладочного и рабочего. Очевидно, что макросы позволяют обобщить эти два варианта программы в одной макропрограмме.
Простой МП может быть построен по однопросмотровой схеме. Непременное условие для её осуществления - размещение МО в начале макропрограммы. Систему данных такого МП составляют три основные таблицы:
Порвые две таблицы строятся при обработке МО, последняя - при обработке каждого МВ.
Структуру таблиц удобно объяснить на конкретном примере, который мы и приводим:
prg3 start 0 sqr macro &data lda &data mul &data sta &data mend movw macro &source,&dest lda &source sta &dest mend
... sqr sigma ... movw alfa,beta ... end prg3
Таблицы NamTab и DefTab для программы примера могут иметь следующий вид.
Имя МК | Начало | Конец | |
1 | sqr | 1 | 3 |
2 | movw | 4 | 5 |
3 | |||
4 |
Поля "Начало" и "Конец" в NamTab определяют местонахождение сответствуюших МО в DefTab.
Оператор |
|
1 | lda #01 |
2 | mul #01 |
3 | sta #01 |
4 | lda #01 |
5 | sta #02 |
6 |
Таблица DefTab содержит простой массив строк из МО. Формальные параметры в целях упрощения дальнейшей обработки закодированы. Код-строка имеет фиксированную длину и состоит из знака "#", с последующим номером параметра.
Таблица ArgTab строится при обработки каждого МВ. Она содержит массив строк-аргументов. Приведём пример таблицы ArgTab для макровызова "movw alfa,beta":
Аргумент |
|
1 | alfa |
2 | beta |
3 |
Логика работы МП достаточно очевидна. Строки входа обрабатывыются последовательно. Если они относятся к МО, то они служат основой наполнения таблиц NamTab и DefTab. Если же очередная строка относится к обычным операторам, то из неё выделяется код операции, который служит аргументом поика в таблице NamTab. Если такой код операции в NamTab отсутствует, то это говорит о том, что текущий оператор является командой ассемблера и без какой-либо обработки направляется на выход МП. Если же очередной оператор это макровызов, то для него строится ArgTab, после чего он заменяется соответствующими строками из DefTab, которые последовательно направляются на выход МП. Попутно происходит подстановка вместо кодов аргументов их фактических значений из ArgTab.
Мы рассмотрели принципы организации простого МП. Сложность МП несколько возрастает, если допустить возможность включения МВ в МО. Если к тому же допустить возможность включения в МО вложенных МО, то сложность МП увеличивается весьма существенно, приближаясь к сложности компиляторов.
В некоторых случаях возникает необходимость использования МВ с большим количеством аргументов, но при этом оказывается, что большинство из них практически всегда имеют одни и те же значения. В таких случаях запись МВ будет более короткой, если соответствие формальных параметров и аргументов устанавливать на основе ключевых слов. Синтаксис формального параметра: &Ключ=[ ЗначениеПоУмолчанию ] . Синтаксис аргумента: Ключ=ЗначениеФактическогоАргумента . Пример.
Вход МП | Выход МП |
prg4 start 0 movw macro &d=alpha,&s=beta lda &s sta &d mend ... movw s=data,d=res ... movw ... end prg4 |
prg4 start 0 ... lda data sta res ... lda beta sta alpha ... end prg4 |
Очевидно, что порядок записи ключевых аргументов может быть произвольным. Если ключевой аргумент опущен, то автоматически принимается значение по умолчанию. Обычно МП допускают и смесь позиционных и ключевых параметров, но первые при этом записывают впереди.
Обычно МП даёт возможность формировать выход буквально из отдельных знаков. Для их сцепления в слова МП обычно предоставляет специальную операцию, обозначим её, например, точкой - . . Пример.
Вход МП | Выход МП |
prg5 start 0 load macro ®,&data ld.® &data mend ... load a,vx ... load x,vy ... end prg5 |
prg5 start 0 ... lda vx ... ldx vy ... end prg5 |
При расширении многократных МВ на основе МО, содержащего метки в модельных операторах, будет получено повторение определений меток, что приведёт к ошибке при последующем ассемблировании. Для устранения этого недостатка был предложен подход с использованием специального системного параметра (обозначим его $), расширение которого зависит от номера МВ в программе, например так: в первом МВ - _aa, во втором - _ab, и т.д. до _zz. Пример.
Вход МП | Выход МП |
prg6 start 0 psem macro &d lda &d comp c0 jeq m.$ sub c1 sta &d m.$ nop mend ... psem sema ... psem semb ... end prg6 |
prg6 start 0 ... lda sema comp c0 jeq m_aa sub c1 sta sema m_aa nop ... lda semb comp c0 jeq m_ab sub c1 sta semb m_ab nop ... end prg6 |
Использование системного знака "_" в расширении системного имени $ обеспечивает невозможность случайного совпадения сгенерированного имени с каким либо проблемным именем. МП обычно предлагает достаточно большой набор подобного рода средств.
Во всех предыдущих примерах количесво операторов в расширении МВ было в точности равно количеству модельных операторов в МО. Хотелось бы иметь более гибкие возможности. Их предоставляют средства условной макрогенерации МП. Характерный пример МП-оператор if, который может иметь, например, такой синтаксис:
$if (ВыражениеПМГ) ПоследовательностьОператоров1 [$else ПоследовательностьОператоров2 ] $endif
Для записи конструкции ВыражениеПМГ (выражение ПМГ - ВПМГ) МП обычно предлагает целый набор операций и переменных периода макрогенерации. МП выполняет оператор if в зависимости от значения ВПМГ: если оно имеет значение true, то на на выход МП направляется ПоследовательностьОператоров1, в противном случае - ПоследовательностьОператоров2. Пример.
Вход МП | Выход МП |
prg7 start 0 movw macro &s,&d $if (&s<>'a') lda &s $endif $if (&d<>'a') sta &d $endif mend ... movw vx,vy ... movw data,a ... end prg7 |
prg7 start 0 ... lda vx sta vy ... lda data ... end prg7 |
Аналогичным образом моогут вводиться и циклические конструкции. Например следующее МО можно использовать для реализации МВ "Возведение в степень".
pow macro &x,&n,&y &cnt set 1 lda &x $while (&cnt<&n) mul &x &cnt set &cnt+1 $endw sta &y mend
Здесь set - оператор присваивания ПМГ, а &cnt - переменная ПМГ. Очевидно, что в МР команда mul будет включена n-1 раз, что и обеспечит решение поставленной задачи. Например вызов
pow d,3,res
будет заменён следующим МР
lda d mul d mul d sta res
Обычно арсенал подобных средств ПМГ примерно соответствует тому, что мы можем найти в ЯВУ.
Постепенно идеи макрообработки проникли и в ЯВУ и иногда в довольно своеобразной форме. В этой связи можно вспомнить о ключевом слово inline и шаблонах в C++. Наиболее заметным средством подобного рода стал препроцессор языка C. Именно благодаря своему препроцессору C продержался на плаву столько лет. Остановимся на некоторых средствах этого славного препроцессора поподробнее, не претендуя на исчерпывающее его изучение.
Оператор #include обеспечивает включение в программу внешних файлов. Примеры.
#include <stdio.h> #include "deffile.h"
Первый оператор выполнит включение в программу текста из файла stdio.h находяшегося в системном каталоге компилятора. Второй оператор включает текст из файла, который находится в текущем каталоге.
Оператор #define используется для записи МО. Примеры.
#define TRUE 1 #define FALSE 0 #define sqr(x) ((x)*(x))
И в общем виде
#define Имя[(СписокПараметров)] МодельнаяСтрока
Примеры МВ sqr и соответствуюших МР.
sqr(a+1) <- ((a+1)*(a+1)) d/sqr(d) <- d/((d)*(d)) sqr(d++) <- ((d++)*(d++))
Последние примеры отвечают на вопрос об обилии скобок в модельной строке МО для sqr. Следует обратить внимание на третий пример. Возможно, что мы получили не совсем то, что хотели.
Оператор #ifdef предоставляет возможность условной препроцессорной обработки. Его синтаксис:
#ifdef Имя Текст [#else Текст ] #endif
В этом операторе условие считается истинным, если Имя предварительно определено, то есть определено одним из операторов #define или указано в специальном ключе командной строки вызова компилятора. Оператор #ifndef аналогичен оператору #ifdef, но условие считается истинным, если имя не определено.
Практический пример.
#ifndef Base_H #define Base_H ОписаниеКласса #endif
Если описание класса оформлено таким образом, то будет выполнено только одно единственное включение файла с описанием класса, даже, если таких попыток будет несколько.
Оператор #if позволяет выразить условие с помощью выражения периода препроцессорной обработки. Его синтаксис:
#if Выражение Текст {#elif Выражение Текст } [#else Текст] #endif
Здесь ключевое слово elif используется в смысле "else if".
Пример.
#if SYS == "IBM" #include "ibm.h" #endif
В заключение приведём ещё один практический пример - макроопределение "Взломщика событий в API Win32".
#define HANDLE_MSG(hwnd, message, fn) \ case (message): return HANDLE_##message((hwnd),(wParam),(lParam),(fn))
В этом примере знак "\" указывает на продолжение оператора в следующей строке, а лексема ## обозначает операцию сцепления параметра с текстом.
Например макровызов в программе
HANDLE_MSG(hwnd, WM_CHAR, Prg_OnChar);
препроцессор заменит на
case (WM_CHAR): return HANDLE_WM_CHAR((hwnd),(wParam),(lParam),(Prg_OnChar));