Что-то мы все о мелком и мелком. Мини, микро - фи. Попробуем что-нибудь побольше. Например, макропроцессоры.

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

Необходимость активного использования макропроцессора для языка иногда считают признаком некоторой незрелости. Вот что пишет по этому поводу Бьерн Страуструп: «Практически каждый макрос свидетельствует о недостатке в языке программирования, программе или программисте. Так как макросы изменяют текст программы до обработки его компилятором, макросы также создают проблемы для многих инструментов разработки... Если вам необходимо использовать макросы, внимательно прочитайте руководство по вашей реализации препроцессора C++ и не пытайтесь искать слишком хитроумных решений... Почти невозможно избежать макросов в одном случае. Директива #ifdef идентификатор заставляет компилятор (при выполнении условия) игнорировать последующий текст, пока не встретится директива #endif... В большинстве случаев использование #ifdef менее загадочно и, при наличии определенных ограничений, приносит мало вреда» [1].

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

Как много правил нужно для того чтобы описать полезный макропроцессор? Оказывается, достаточно мало. Копаясь в литературе, я когда-то нашел описание макропроцессора GPM от Стречи (приведенные примеры взяты из [2]). Макровызов в GPM можно описать в следующей форме:

# имя_макро {, параметр};

Описание макро делается в виде обращения к специальному макро DEF. При этом для обращения к параметрам используется запись вида &1 ... &9. Для защиты от немедленной обработки (например, для ввода символов «,» или «&» как таковых) используются скобки <> (соответственно надо указывать <,> или <&>). Примеры макро: #DEF,ADD,<&1+&2+#PI;>; или #DEF,A,B;

Теперь опишем сам процесс макроподстановки. Он задается пятью правилами.

  1. Сначала вычисляются все параметры макроподстановки (в частности, имя макро). Естественно, при «вычислении» параметров, возможно, будут выполнены другие макровызовы.
  2. В таблице макроопределений, которая просматривается с конца (более поздние описания «закрывают» предыдущие), находим имя макро. Если его нет, такая ситуация рассматривается как ошибка.
  3. В стандартном для макропроцессора стиле просматриваем тело макро. Любой макровызов немедленно обрабатывается, а вместо аргумента &n подставляются результаты первого этапа (т.е. вычисленные параметры).
  4. После обработки тела макро все параметры (и добавленные макро - это весьма существенно!), которые мы вычислили на первом этапе, уничтожаем.
  5. Продолжаем просмотр входного текста.

Просто? Очень просто! Казалось бы, нет ни обработки условий, ни возможности вычислений. Зачем он такой нужен? Как это не удивительно, но все это есть. Более того, для этого ничего нового и не требуется - уже все предусмотрено.

Пример реализации условий. Пусть x, y, v, w - цепочки символов (вместо них, естественно, вы можете, например, подставить вызов макро для «переменных» и т.п.):

#x,#DEF,x,;#DEF,y,;;

реализует стандартную проверку if x=y then w else v

Почему?

Если x и y совпадают, то второй #DEF переопределит значение макро для x. Догадываетесь, что же реально появится на месте подстановки #x?... Вместо v, которое у нас обозначает действие для else, отработает вариант w! Забавно, не правда ли?

Пример вычисления:

#DEF,SUC,<#1,2,3,4,5,6,7,8,9,10,#DEF,1,
<&>&1;;>;

Сей «страшный» макро умеет прибавлять единицу! Например, посмотрим результат #SUC,3;

При обработке макро получим:

#1,2,3,4,5,6,7,8,9,10,#DEF,1,<&>3;;

Теперь бы нам надо выполнить макро #1. Но его определение задано тут же. Тело макро после обработки будет &3. Так, что это у нас идет третьим параметром макровызова 1 - 4. Отлично. Макровызов #SUC,3; дает в выходной поток 4.

В аналогичном стиле можно организовать работу с двузначными числами и т.д.

Сейчас, возможно, GPM стоит рассматривать скорее как курьез, чем как серьезный инструмент. Однако согласитесь, как, оказывается, просто привнести в систему с макроподстановками побочные эффекты!

Не все макропроцессоры столь тривиальны по своему описанию, как GPM или препроцессор языка Cи. Например, лет десять назад, реализовав макропроцессор ТРАК [3], мой коллега Александр Бушмелев получил великолепный инструментарий для быстрого написания игр в стиле «Приключения» (это был, конечно, терминальный вариант), с вводом команд на «естественном» языке и т.п. Естественно, и в этой игре обнаружились побочные эффекты (типа возможности ввести имя нужного макро для изучения «секретов»). В программировании для таких мощных макропроцессоров всегда есть нечто от функционального программирования и для всех макропроцессоров всегда есть место для разнообразных цирковых трюков, побочных эффектов и т.п.

Рассмотрим, например, пару таких побочных эффектов в shell, которые связаны с управлением макроподстановками.

$ set — abc def gh ijk
$ IFS=»,»
$ echo «$*»
abc,def,gh,ijk
$ echo «$@»
abc def gh ijk
$ IFS=»/»
$ bar=abc/def
$ $bar
bash: abc: command not found

Как видите, переменная IFS переопределяет поведение shell в некоторых ситуациях. Вторая из ситуаций (IFS=/), как вы понимаете, может даже превратится в боевое заклятье и создать проблемы с безопасностью. Для этого достаточно, чтобы какая-то программа (с SUID) запускала «пробиваемый» shell-скрипт, или в системе допускалось существование механизма SUID-скриптов. Дело настолько серьезно, что IFS упоминается в книгах по «секьюрити», например [4]: «New editions of Unix sh ignore IFS if the shell is running as root or if the effective user ID differs from the real user ID» (другими словами, новые редакции sh игнорируют IFS, если shell выполняется от имени суперпользователя или действующий идентификатор пользователя отличается от фактического) 4. Любопытно, что даже редактор строки в shell упоминается как потенциально опасный в некоторых режимах работы (кстати, как вы думаете, почему?).

Следующий пример — из shell (например, из «линуксовского» bash):

set — keys=2,5
cat << OK
Our current options are — $1 -
OK
cat << OK
Our current options are — $1 -
OK

Ясно, что поводов защищать от обработки просто букву O, как правило, нет (это же не специальный символ!). Но результат-то отличается. Последствия оказываются достаточно значительными.

Другие примеры связаны с использованием препроцессора языка Cи. Заметим, что этот препроцессор очень часто используется для целей, не имеющих никакого отношения к самому языку Си. Например, препроцессор Си используется для обработки базы данных ресурсов некоторыми графическими оболочками (см., например, ключи программы xrdb).

Необходимо также отметить, что, конечно, Страуструп прав и макроподстановки потенциально опасны. Тривиальные примеры показывают это:

#define mul(a,b) a*b
#define min(a,b) ((a)<(b))?(a):(b)

Что будет, если попытаться выполнить подстановку mul(1+2,4). Ясно, что нам хотелось бы получить (1+2)*4, но реально-то мы имеем 1+2*4, а это, заметим, несколько иное число. Нам явно не хватает скобок. Увы, второй пример показывает, что и скобки не дают требуемый результат. min(*a++,*b++) приводит (за счет побочного эффекта) к непредсказуемому результату. Т. е. даже простейшее макро требует, вообще говоря, некоторой поддержки на уровне языка, например, в следующей форме:

__typeof__ (a) __a = (a); ...

Еще сложнее ситуация при написании процедур с переменным числом параметров (типа printf). Ясно, что когда часть параметров лежит в обычных регистрах, часть в плавающих, а часть в стеке (типичная ситуация для RISC-процессоров), то процедурка «получить следующий параметр» превращается в весьма нетривиальный макрос с существенной поддержкой компилятора (типа появления __builtin_va_alist и т.п.). Впрочем, если говорить о поддержке возможностей языка компилятором, то полезно взглянуть на компилятор языка Ада. Снова цитата: «...любой компилятор языка Ада обладает встроенным калькулятором произвольно большой точности (с точностью в пределах возможностей конкретной машины) и можно только сожалеть, что этот калькулятор не доступен при выполнении программ» [5]. В книге приводится пример, когда компилятор обязан, например, вычислить 5**(2**31) mod 2**(2**5)+1 (и то, и другое число, естественно, за пределами разрядной сетки, ясно, что первое, в принципе, великовато и для «калькулятора», поэтому вычисление mod делается за 5 шагов) просто для порождения кода условия. Самой программе остается только выдать ответ в форме «факт верен» или «нет». Написание соответствующей программы просто на Аде потребовало бы специальной библиотеки.

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

#ifndef MY_SUPER_TYPES
#define MY_SUPER_TYPES
typedef .....
#define .....
#include ....
const .....
#endif /* MY_SUPER_TYPES */

Ясно, что повторное обращение к данному файлу безвредно.

Однако достаточно популярно использование и нерекурсивного многократного вызова одного файла. Это помогает в том случае, когда нам необходимо «синхронизовать» изменение имен, действий, данных и т.п.

Рассмотрим простой пример:

#define r_ADD		0
#define r_SUB		1
#define r_SUMMARY	2
int length_of_record[]={4, 12, 66};
......
	switch(code_of_record){
	case r_ADD:
		/* code for A-record */
		break;
	case r_SUMMARY:
		.....
	}
	OutputNewRecord(pR,
 length_of_record[code_of_record]);
....

В данном случае у нас есть желание иметь какие-то разумные имена для типов записи, массив длин и код обработки для записи требуемого вида. Что будет, если нам захочется изменить набор записей, и, соответственно, добавить или удалить какой-либо тип (с перенумерацией), если типов записей будет не три и не пять, а пара-другая десятков. Ясно, что возможно расхождение между, например, элементами массива и именами. Как бы это «автоматизировать» так, чтобы расхождения не возникали или отлавливались бы компилятором? Вот для этих-то целей и полезны многократные проходы по файлам. Наш тривиальный пример, в частности, переписывается так:

	..... Файл OurDesc.h
RecDes(r_ADD,		4)
RecDes(r_SUB,		12)
RecDes(r_SUMMARY,	66)

.... Файл MyProg.c
#define RecDes(name, length) name,
enum{
#include «OurDesc.h»
TotalRecordTypes};
#undef RecDes
#define RecDes(name, length) length,
int length_of_record[]={
#include «OurDesc.h»
0};

В данном случае мы не трогали switch и занимались только синхронизацией имен и массива. Ясно, что добавление и удаление строк в OurDesc.h приводит к корректному и согласованному изменению имен и массива. Мы видим простейший вариант применения этого приема. В общем случае, например, несколько файлов используют только имена из OurDesc.h, а другие - собственные компоненты и т.д.

Попробуем задать себе следующую задачку (взята из одной конференции на сервере infoart и слегка переформулирована): как в рамках одной программы можно при помощи макро автоматически выделять уникальные имена для организации доступа к каким-либо ресурсам. Одним из требований является неизменность имени при вставке/удалении фрагментов текста, что автоматически блокирует возможность использовать __LINE__. Ясно, что в общем случае данная задача не решается. Если мы попытаемся ввести такое макро INCR, которое можно было бы использовать несколько раз, то вставка его где-либо в середину текста привела бы к изменению имен ресурсов лежащих ниже данной позиции. Предположим, что это не так, внутренних вставок нет, и попробуем решить задачу. Гм, однако, есть проблемы. Одна из них заключается в том, что нам нужно уметь считать при помощи препроцессора C, а другая в том, что результат должен быть представлен строкой с вставкой __FILE__ (для уникальности). Проблема счета почти фатальна. Ясно, что никакое макро INCR не имеет возможности поменять что-то во внешнем окружении так, чтобы при следующем вызове появилось новое значение. Нам бы подошло что-либо в стиле sprintf(newname, «%s%d», __FILE__, nlabel++), но выполненное на этапе компиляции, поскольку мы не можем гарантировать порядок выполнения запросов в файле. Увы, вычисляемого nlabel у нас нет.

#define nlabel nlabel+1

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

New_name =
#include «NewName.h»

В этом случае возможно совершенно тупое решение:

———— файл NewName.h
#ifndef __NCounter
	__FILE__ «0»
#define __NCounter 1
#elif __NCounter==1
	__FILE__ «1»
#undef __NCounter
#define __NCounter 2
#elif __NCounter==2
	__FILE__ «2»
#undef __NCounter
#define __NCounter 3
#endif
———— файл newname.c
main()
{
	printf(«%s
»,
#include «NewName.h»
	);
	printf(«%s
»,
#include «NewName.h»
	);
	printf(«%s
»,
#include «NewName.h»
	);
}

Очевидно, что будут выведены три разных строчки. Однако здесь явно использовано, что компиляторы ANSI С склеивают строки типа «abc» «def» в одну «abcdef». При соответствующем желании (не используя слишком много строк) можно заставить NewName.h порождать сотни имен.

Поговорим теперь еще об одном языке скриптов - TCL. Вы можете попробовать поработать с ним при помощи команды tclsh.

% if 0 {puts stdout a} else {puts stdout b}
b

В таком виде мы обнаруживаем почти Си-нотацию. Однако это не так. Это доказывается следующими строками:

% set a {puts stdout $a}
% set b {puts stdout $b}
% $a
invalid command name «puts stdout $a»
% if 1 $a else $b
puts stdout $a
% if 0 {skoriki-moriki o-go-go} {puts
 stdout «All OK»}
All OK
% if 1 «puts stdout {Optimal form}»
 else [puts stdout «Other form»]
Optimal form

Как видите, {} работают аналогично кавычкам, даже круче. Это особенно хорошо видно в последнем примере. Предпоследний пример показывает, что разбор строки производится только в том случае, если это действительно необходимо, т. е. в том случае, когда, например, надо выполнить соответствующую ветку конструкции if. Если же веточка не выполняется, мы не выполняем ее и не имеем никакой диагностики по поводу синтаксиса и т.п. Нет, граждане, Shell ведет себя приличнее.

Первые примеры показывают стандартные фокусы с макропроцессором, т.е. возможность запомнить и выполнить далее некоторый кусок кода из переменной. В общем случае для этого может потребоваться команда типа eval (shell, perl, tcl и т.п.).

Пример с обращением к команде $a (без eval и т.п.) четко показывает, что a хранится в «неразобранном» виде. Следующий пример показывает, как a распечатывает себя.

Еще один макропроцессор, который активно используется в Unix, — это, естественно, m4. Это макропроцессор общего назначения. Он может использоваться для решения достаточно произвольных задач, например, для генерации разнообразных скриптов, программ, «компактного» конфигурирования sendmail (sendmail.mc->sendmail.cf).

Чем интересен m4? Для макропроцессоров общего назначения (например, конфигурирования) стандартной проблемой является точное соблюдение формата генерируемого файла. Например, зачем бы вам потребовались строки типа #line, которые оставляет обычно после себя препроцессор Си? Ну, это-то, положим, ясно: можно отключить. А вот куда, например, девать огромное количество пустых строк и проч.? Кроме того, естественно, если конфигурационные параметры имеют достаточно общий характер, то генерируемый код должен быть в виде различных кусочков разбросан по файлу - например, определения типов отдельно, код отдельно и т.п. m4 позволяет делать все это, например, переключение выходных потоков выполняется по divert. Правда удалять лишние строки приходится «вручную», добавляя в конце вызов dnl, так как dnl удаляет после себя все до конца строки, то, соответственно, он превращается и в «символ комментария». Существенно и то, что m4 содержит явные макросы для операций с числами, например: incr, decr, eval. Имеются действия с подстроками (чего нет, например, в препроцессоре Си или GPM), вызов команд, отладочные команды и т.п.

Т.к. в принципе все представимо в виде действий над текстом, то мощные макропроцессоры могут все, и даже простейшие могут намного больше, чем кажется, но иногда... как же медленно они все это делают!..

А у нас в квартире газ...

В этом году мне попалось несколько руководств для пользователей ОС Unix с достаточно странными предложениями попрограммировать на Си. Что уж тут странного, можете спросить вы. Проблема в том, что программы были нужны просто для перевода чисел из одной системы счисления в другую. Типа шестнадцатеричное в десятичное, восьмеричное и наоборот. Но для этого в Unix уже есть средства, а компилятор Си установлен не на всех компьютерах.

Для перевода чисел из одной системы в другую можно использовать программу bc. Эта программа может выполнять арифметические действия в различных системах счисления (конечно, я упрощаю — см. описание, но главное, мы можем вводить и выводить числа в разных системах счисления). При этом вводить можно в одной, а выводить в другой. Для управления вводом/выводом используются команды ibase и obase. Например:

	bc
	ibase=2
	101 - ввод двоичного
	5 - вывод программы в десятичном виде
	obase=5
	1010 - вводим двоичное 10
	20 - получаем 5-ричное значение
	ibase=16
	obase=10
	AF - размер имеет значение
	175

Естественно, это все легко превращается в командный файл для любого типа преобразования.

Bc позволяет не только тривиальные действия типа перевода или вычисления, но и определение достаточно сложных функций. При этом вычисления ведутся точно, т.е., например, при помощи программы вычисления факториала, которую вы можете обнаружить в руководстве (наберите man bc). Вам ничто не мешает найти точно 120!

Свои комментарии вы можете направлять автору по адресу: oblakov@bigfoot.com

Литература

[1] Б. Страуструп, Язык программирования C++, третье издание, 1999. Раздел 7.8 «Макросы»

[2] Д. Грис, Конструирование компиляторов для цифровых вычислительных машин. М.: «Мир», 1975

[3] Ч. Уэзерелл. Этюды для программистов, М.: «Мир», 1982

[4] Simon Garfinkel and Gene Spafford, Practical UNIX and Internet Security, Second Edition, O?Reilly & Associates, Inc., p. 125

[5] П. Ноден, К. Китте, Алгебраическая алгоритмика. М.: «Мир», 1999, стр. 62

Поделитесь материалом с коллегами и друзьями