If a C programmer asks "do you want to see something cool?", run away.
--John Van Enk

Friday, January 12, 2007

Flex & Bison C++ Interoperability - Intro

Уф давненько я сюда не заглядывал - уже и забыл что у меня есть блог :)! Вот собственно по теме - есть такие замечательные программы как Flex (GNU аналог Lex'а) - генератор лексических анализаторов и Bison (аналог YACC'а - Yet Another Compiler Compiler - "еще один" компилятор компиляторов) - генератор синтаксических анализаторов (парсеров). Каждый из этих тулзов получает на вход файл спецификаций и после обработки его выдает на выходе код на С (можно и на С++, ведутся работы над Java - но об этом дальше) который затем можно собрать и в итоге получить программу осуществляющую грамматический разбор текста и выполняющую определяемые разработчиком действия. В основном данные средства используются в паре так как парсер, генерируемый Bison требует еще наличия функции со ледующим интерфейсом (в простейшем случае):
yylex()
которая собственно скармливает парсеру лексемы, которые тот уже группирует в различного рода выражения. Именно эту функцию и с таким интерфейсом генерирует по умолчанию Flex если генерировать код на С. Но мне не сильно хотелось "опускаться" до уровня С (не моймите меня неправильно не имею ничего против С как языка - просто я люблю С++ :)

Flex можно найти на flex.sourceforge.net
Bison можно найти на www.gnu.org/software/bison
Там же ссылки на мануалы и кроме того при установке тулзов становится доступна довольно качественная документация в виде manpages & info pages (это так если вдруг кто захочет поподробнее покопаться, позже приведу ссылки на ресурсы в сети - как только выкопаю их где то у себя в закладках :).

Так вот, неожиданно вдруг у меня появилась задача написать простенький интерпритатор и решил я для этого использовать вышеупомянутую парочку. Кроме всех прочих требований и бла-бла-бла были требования о thread safety &reentrancy. Естественно, сгенерированные по умолчанию ни лексер ни парсер этим условиям не удовлетворяют (если использовать С в качестве target language) - но вот как бы насколько я помню этим условиям удовлетворяют классы С++ (при некоторых оговорках). Оба инструмента поддерживают возможность генерации классов С++, но они не стыкуются по интерфейсам так как парсер все так же требует функции лексического анализа со следующей сигнатурой вызова
yylex (semantic_type* lval [,location_type* lloc])
Притом второй аргумент присутствует лишь в том случае если используется location tracking (отслеживание позиции).
Flex обеспечивает 2 способа генерации сканеров для их последующего использования в программах на С++. Первый - просто скомпилировать сгенерированный сканер при помощи С++ компилятора - однако необходимо некоторые функции при этом обьявить как
extern "C"
Такой способ мне не подошел по вышеупомянутым причинам (одной non reentrancy хватило). Второй - сгенерировать сканнер на С++, который представляет собой реализацию классов, интерфейс которых определен в заголовочном файле FlexLexer.h, который в свою очередь устанавливается в системе вместе с Flex. Вот он (за исключением неинтересующих на данном этапе функций-членов классов):

#ifndef __FLEX_LEXER_H
#define __FLEX_LEXER_H
extern "C++" {
class FlexLexer {
public:
virtual ~FlexLexer() { };
virtual int yylex() = 0;
int yylex( FLEX_STD istream* new_in,
FLEX_STD ostream* new_out = 0 );
};
}
#endif

#if defined(yyFlexLexer) || ! defined(yyFlexLexerOnce)
#define yyFlexLexerOnce

extern "C++" {

class yyFlexLexer : public FlexLexer {
public:
yyFlexLexer( FLEX_STD istream* arg_yyin = 0,
FLEX_STD ostream* arg_yyout = 0 );
virtual ~yyFlexLexer();
virtual int yylex();
};
}
#endif


Если обратить внимание на стражи включения (#ifndef - #define-#endif) то можно заметить что при повторном включении данного заголовочного файла в одну и ту же единицу трансляции базовый класс FlexLexer повторно вкючен не будет а вот его наследник yyFlexLexer при определенных условиях (о них в следующих постах) может быть включен поторно, что естественно приведет к ошибке компиляции.

Итак о "стыковке" парсера и лексера. Наиболее простым решением , на мой взгляд, является создать наследника от yyFlexLexer и добавить в него
operator()(semantic_type* lval ,location_type* lloc)
и далее передавать екземпляр унаследованного класса в конструктор парсера (благо такую возможность предусмотрели разработчики Bison). Но параметры lval & lloc должны быть доступны внутри yylex(). Далее, читая мануалы, наткнулся на возможность указать в опциях генерации буквально следующее "сгенерировать реализацию функции yylex() для класса унаследованного от yyFlexLexer". Естественно имя унаследованного класса указывается в качестве параметра.

Ну вот в принципе и решение проблемы:
1) наследуем от yyFlexLexer класс с оператором
operator()(semantic_type* lval ,location_type* lloc)

2) указываем "сгенерировать реализацию yylex() для класса унаследованного от yyFlexLexer"

3) указываем что конструктор парсера принимает дополнительный параметр с идентификатором yylex ( внутри парсера вызывается функция yylex - таким образом при вызове
yylex(yylval, yylloc)
на самом деле будет вызван
 yylex.operator()(yylval,yylloc) )

Но есть еще some tricks которые необходимо проделать чтобы все что в итоге нагенерировалось скомпилировать... :) О них напишу чуть позже.