Содержание

Макропроцессоры

1. Основные определения
2. Позиционные параметры
3. Соотношение понятий "макрос" и "подпрограмма"
4. Структуры данных и логика работы макропроцессора
5. Ключевые параметры
6. Конкатенация параметра с текстом
7. Генерация уникальных меток
8. Условная макрогенерация
9. Препроцессор языка C

Макропроцессор (МП) - системная программа преобразования текста в соответствии с имеющимися в нём инструкциями. Основное назначение - повышение уровня программирования на основе подстановки текста. Впервые стали применяться для построения ассемблерных программ. Совмещение ассемблера с МП называется макроассемблером. Макропрцессоры также могут встраиваться в ЯВУ. В этом случае МП называется препроцессором.

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

1. Основные определения

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

Определим возможный синтаксис записи рассмотренных конструкций и приведём примеры.

Синтаксис МВ:

[<метка>]   <имя_МВ>  [<список_фактических_параметров>]

Синтаксис МО:

[<имя_МВ>]   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 заменяется соответствующим МР, которое строится на основе модельных операторов МО. Фактически в  данном случае мы видим пример простой подстановки. Если на входе МП мы имеем макропрограмму с МО и МВ, то на его выходе мы уже получаем чистую ассемблерную программу, которую, в свою очередь, можно подать на вход обычного ассемблера.

Несколько слов о месторасположении МО. Удобнее всего их размещать в начале макропрограммы. Они также могут размещаться в специальных системных библиотеках МО. Такие МО называют системными. В том случае, если МО из макропрограммы конфликтует с системным МО, то обычно для МР используется МО из макропрограммы.

В литературе наряду с введёнными терминами используют их синонимы: МО - макрос, МВ - макрокоманда, МР - макроподстановка.

2. Позиционные параметры

Параметры позволяют модифицировать МО. Приведём пример.

Вход МП Выход МП
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

Синтаксис параметра: &<имя>

В этой связи символ "&" приобретает в МП статус системного.

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

3. Соотношение понятий "макрос" и "подпрограмма"

Из приведенных примеров уже ясно, что техника МП-обработки напоминает работу с подпрограммами. У этих двух подходов много общего, но всегда следует помнить, что подпрограмма - это средство периода выполнения программы, а макрос - периода макрогенерации (ПМГ). Так как оба подхода обечпечивают повышение уровня программирования, то возникает вопрос о их взаимозаменяемости. На этот вопрос следует ответить утвердительно - подпрограмму можно заменить на макрос и наоборот. И сразу же возникает ещё один вопрос: "В каких случаях следует предпочесть тот или иной подход"? Чтобы на него ответить выполним кое-какие расчёты. Сначала выполним сравнение для конкретного примера, а потом обобщим полученные результаты. Критерии сравнения - объём кода и время выполнения программы.

  Макрос Подпрограмма
Вызов
      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 раз!

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

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

Простой МП может быть построен по однопросмотровой схеме. Непременное условие для её осуществления - размещение МО в начале макропрограммы. Систему данных такого МП составляют три основные таблицы:

Порвые две таблицы строятся при обработке МО, последняя - при обработке каждого МВ.

Структуру таблиц удобно объяснить на конкретном примере, который мы и приводим:

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.

Мы рассмотрели принципы организации простого МП. Сложность МП несколько возрастает, если допустить возможность включения МВ в МО. Если к тому же допустить возможность включения в МО вложенных МО, то сложность МП увеличивается весьма существенно, приближаясь к сложности компиляторов.

5. Ключевые параметры

В некоторых случаях возникает необходимость использования МВ с большим количеством аргументов, но при этом оказывается, что большинство из них практически всегда имеют одни и те же значения. В таких случаях запись МВ будет более короткой, если соответствие формальных параметров и аргументов устанавливать на основе ключевых слов. Синтаксис формального параметра: &Ключ=[ ЗначениеПоУмолчанию ] . Синтаксис аргумента: Ключ=ЗначениеФактическогоАргумента . Пример.

Вход МП Выход МП
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

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

6. Конкатенация параметра с текстом

Обычно МП даёт возможность формировать выход буквально из отдельных знаков. Для их сцепления в слова МП обычно предоставляет специальную операцию, обозначим её, например, точкой - . . Пример.

Вход МП Выход МП
prg5    start   0			
load    macro   &reg,&data
        ld.&reg &data
        mend
        ...
        load    a,vx
        ...
        load    x,vy
        ...
        end     prg5
prg5    start   0
        ... 
        lda     vx
        ...
        ldx     vy
        ...
        end     prg5

7. Генерация уникальных меток

При расширении многократных МВ на основе МО, содержащего метки в модельных операторах, будет получено повторение определений меток, что приведёт к ошибке при последующем ассемблировании. Для устранения этого недостатка был предложен подход с использованием специального системного параметра (обозначим его $), расширение которого зависит от номера МВ в программе, например так: в первом МВ - _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

Использование системного знака "_" в расширении системного имени $ обеспечивает невозможность случайного совпадения сгенерированного имени с каким либо проблемным именем. МП обычно предлагает достаточно большой набор подобного рода средств.

8. Условная макрогенерация

Во всех предыдущих примерах количесво операторов в расширении МВ было в точности равно количеству модельных операторов в МО. Хотелось бы иметь более гибкие возможности. Их предоставляют средства условной макрогенерации МП. Характерный пример МП-оператор 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

Обычно арсенал подобных средств ПМГ примерно соответствует тому, что мы можем найти в ЯВУ.

9. Препроцессор языка C

Постепенно идеи макрообработки проникли и в ЯВУ и иногда в довольно своеобразной форме. В этой связи можно вспомнить о ключевом слово 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));