Сообщений 6    Оценка 45        Оценить  
Система Orphus

REB - интерпретатор для низкоуровневого программирования.

Regular Expression for Binary data processing

Автор: Осман Бинеев
Опубликовано: 24.10.2001
Исправлено: 13.03.2005
Версия текста: 1.1

Введение
Регулярные выражения
Проект REB
Шаблоны
Переменные
Хэш
Ссылки
Объекты
Заключение

Демонстрационная программа - 405 KB
Исходные тексты - 42 KB
REB.sourceforge.net

Введение

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

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

Решение проблемы, достаточно простое и быстрое, пришло мне в голову когда я чисто из спортивного интереса изучал такой замечательный скрипт-язык, как Perl. Там высокая продуктивность работы программиста достигается использованием регулярных выражений (regular expressions). Являясь достаточно сложными для прочтения, регулярные выражения позволяют записать в одной короткой строке достаточно сложный алгоритм, реализация которого на С/С++ потребовала бы куда больших временных затрат. Конечно, в регулярном выражении легко ошибиться. Но зато и легко найти ошибку, именно в силу обозримости кода.

Регулярные выражения

Здесь небходимо сделать краткое отступление в сторону регулярных выражений. С простейшими из них мы сталкиваемся когда занимаемся поиском файлов по расширению: запись "*.cpp" означает, что мы хотим найти все файлы, имена которых состоят из последовательностей произвольных (допустимых) символов, суффиксированных расширением ".cpp". Символ "*" обычно называется квантором, и таких кванторов в языке Perl несколько, хотя они имеют другой смысл.

Символ "." предполагает одно и только одно повторение любого символа. Квантор "*", стоящий после какого-либо символа (или последовательности символов, заключенных в скобки), говорит о произвольном (в том числе и нулевом) количестве повторений символа или последовательности. Например, шаблону (abc.)* соответствуют такие последовательности, как "abcD", "abcfabc9abcn", или даже последовательность "", не содержащая никаких символов вообще.

Еще два квантора: "+" - одно или несколько повторений, "?" - ноль или одно повторение символа или последовательности. Более того, вы можете задать любое нужное вам количество повторений, используя фигурные скобки, например {6} означает ровно 6 повторений, {5,} - от пяти повторений до бесконечности, {3, 80} - от трех до восьмидесяти повторений.

Можно задавать также и семейства символов. Например, [a-zA-Z] означает ни что иное, как произвольный символ латинского алфавита, а [^a-zA-Z] - напротив, произвольный символ, не входящий в латинский алфавит. И, наконец, символ "|" позволяет вам задавать альтернативные шаблоны, например шаблон (c|cpp|h) даст совпадение для любой из трех последовательностей "c", "cpp" и "h".

Проект REB

В таком духе я и начал разрабатывать REB. Сама аббревиатура REB означает "Regular Expressions for Binary Data processing", т.е. регулярные выражения для обработки бинарных данных. Не буду пока углубляться в разъяснение особенностей реализации REB, скажу только, что набор кванторов практически повторяет набор кванторов языка Perl. Основное бросающееся в глаза отличие - то, что основной информативной единицей шаблона REB является не символ, как в Perl, а байт, выраженный двумя шестнадцатиричными символами. Впрочем, и символ и символьная строка могут быть использованы, только символ должен быть заключен в одинарные, а строка - в двойные или обратные кавычки, например как в этом шаблоне: <0a0d "hello, world\n"* 096a6b>. Замечу, что квантор "*" относится в этом примере ко всей строке "hello, world\n". Если нужно проверить повторения символа "\n", то необходимо переписать эту строку так: <0a0d "hello, world" '\n'* 096a6b>. Пробелы, лежащие в "шестнадцатиричной" области, то есть вне кавычек стринга или символа, игнорируются.

В целом разработка шла в духе Perl, однако в некоторый момент оказалось, что логика организации языка REB приводит к еще одному замечательному свойству регулярных выражений, отсутствующему в Perl: к их рекурсивности. В Perl вы можете вставить в ваш шаблон какую-либо переменную, например так:

$hwstring = "hello, world";
$template = "($hwstring|hello)*";

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

Совершенно иная ситуация в REB, где шаблоны являются не строками - пусть даже параметризованными, - а, фактически, функциями. Но здесь пришло время предложить вашему вниманию короткий пример кода. Сразу скажу, что каждая переменная в REB является в то же время функцией, присваивающей значение этой переменной, поэтому запись "x(5);" или даже "x 5;" означает просто-напросто присвоение переменной x значения 5.

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

sp <20 | 09 | 0d | 0a>;

Теперь определимся с тем, что у нас может быть идентификатором:

id <['a'-'z', 'A'-'Z', '0'-'9']+>;

Напомню, что квантор "+" означает одно или более повторение символа. Теперь зададим хитрый шаблон для аргумента:

argum <$exp ($sp)* | $id ($sp)*>;

Здесь значок "|", как и в Perl, разделяет альтернативные шаблоны, т.е. обладает своим общепринятым смыслом символа "или". Хитрость же состоит в том, что шаблон exp еще не определен вообще. В REB это вполне допустимо. Определяем шаблон для списка аргументов:

list <$argum ')' | $argum $list>;

Как видим, приведенный шаблон является рекурсивным, т.е. в нем используется обращение к самому себе. Но пойдем дальше, вот шаблон для функционального выражения - тот, что мы уже использовали в шаблоне arg:

exp <$id '(' $list>;

Вот и все! Теперь можно задать пример для разбора:

sample "script(do(something) make(something else plus(12 7)))";

и проверить, совпадает ли он с шаблоном exp:

result get exp, sample;
if less(result, 0), print "Failure.\n";
else print "OK!\n";

Здесь функция get вызывается для проверки совпадения шаблона exp с примером sample. Результат возвращается в переменную result, которая будет иметь значение >=0 в случае совпадения. Итак, весь текст парсера:

sp <20 | 09 | 0d | 0a>;
id <['a'-'z', 'A'-'Z', '0'-'9']+>;
argum <$exp ($sp)* | $id ($sp)*>;
list <$argum ')' | $argum $list>;
exp <$id '(' $list>;
sample "script(do(something) make(something else plus(12 7)))";
result get exp, sample;
if less(result, 0), print "Failure.\n";
else print "OK!\n";

Конечно, такой парсер пока бесполезен, поскольку мы не можем его использовать для чего-то более сложного, нежели чем проверка факта совпадения с шаблоном. Хотелось бы иметь доступ к тем частям разбираемых данных, которые дали нам совпадение с предоставленным шаблоном. Это вполне возможно, достаточно определить свою функцию, которой будут передаваться результаты разбора:

function printarg, . {
    arg a;
    print "argum: $a\n";
};

и преобразовать шаблон argum следующим образом:

argum <(~printarg ($exp ($sp)*| $id ($sp)*))>;

Тильда после открывающей скобки означает то выражение, которому (через стек) будет передан совпавший участок данных в ходе разбора. Поэтому, будучи преобразованным, наш скрипт выдаст на консоль следующую последовательность:

argum: something
argum: do(something) 
argum: something
argum: do(something) 
argum: something 
argum: something 
argum: else 
argum: else 
argum: 12 
argum: 12 
argum: 7
argum: plus(12 7)
argum: make(something else plus(12 7))
OK!

Посмотрим на то, как продекларирована функция printarg. Это выглядит несколько странно, но легко объясняется. Дело в том, что в языке REB принципиально отсутствуют ключевые слова, хотя есть определенный набор встроенных функций. Одна из таких функций - "function", извините за невольный каламбур. У нее 2 аргумента: название декларируемой функции и тело функции. Тело определяется с помощью другой встроенной функции, название которой состоит из точки и пробела. Поэтому если обозначить функцию, декларирующую тело, идентификатором body, то все можно переписать так:

function(printarg, body(arg(a), print("argum: $a\n")));

Синтаксически такая запись вполне приемлема и понятна интерпретатору, хотя идентификатор body нужно все-таки убрать:

function(printarg, . (arg(a), print("argum: $a\n")));

Возвращаясь к шаблонам, должен вам сказать, что нынешний способ использования функций, встраиваемых в шаблон, не вполне меня удовлетворяет. Лучше было бы передавать встроенной функции 3 аргумента: ссылку на разбираемые данные, смещение первого символа из совпавшего участка данных и длину участка. Это будет более экономно и удобно в использовании.

Шаблоны

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

bitmap <$header $palette $image>;

Пусть заголовок файла у нас состоит из названия формата, номера версии в бинарном виде: 0x0001, количества байт в одной строке растра и количества строк:

header <`OurBitmap` 0001 $line_len $lines>;

Палитра будет состоять из двух частей: количества цветов и самой таблицы цветов:

palette <$num_of_colors $table>;

Теперь опишем конкретный образ следующим образом:

line_len 10; //количество байт в одной линии растра
lines 10; //количество линий
num_of_colors set <02>; //количество используемых цветов - 2
table set <00 00000000 11 00ffffff>; //палитра включает лишь два цвета: черный и белый
image set <
11 11 11 11 00 00 00 11 00 00
11 00 00 11 00 00 11 11 00 00
11 00 00 11 00 11 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 00 00 11 00 00 00 11 00 00
11 11 11 11 00 11 11 11 11 11
>;

Теперь для того, чтобы записать готовый бинарный образ файла в переменную bmpfile, достаточно записать:

bmpfile set bitmap;

Функция set предназначена для формирования бинарных данных из шаблона. Заметим, что тот же шаблон bitmap, после незначительных преобразований, можно будет использовать и для обратной операции - распознавания нашего растрового формата и разбиения его на информативные части.

Далее я опишу некоторые синтаксические особенности REB, а также такие встроенные возможности, как хэши, массивы, ссылки и поддержку объектно-ориентированного программирования.

Переменные

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

В случае, когда интерпретатор не может найти описанную ранее или встроенную функцию под указанным именем, он заводит новую переменную под этим именем и присваивает ей значение, исходя из контекста. Например, строка

mystr "Hello!";

приведет к появлению новой переменной mystr со строковым значением "Hello!". Идентификаторы в REB могут состоять как из обычных символов (т.е. букв латинского алфавита, цифр и символа подчеркивания), так и из специальных символов, предваренных точкой. Например, встроенная функция "less" имеет другой способ записи - ".<". Описанная выше функция описания тела функции имеет только одно специальное имя ". ", состоящее из точки и пробела.

Любой идентификатор может быть предварен символом ":" без какого-либо изменения смысла. Поскольку все переменные REB хранятся в специальном внутреннем хэше, то такие, например, записи идентификатора "less" совершенно эквивалентны:

less
:`less`
:{`less`}

Более того, вы можете задать имя переменной, состоящее из достаточно произвольных символов, заключив их в обратные кавычки, например так:

:`@#$\n` 254;

что приведет к появлению переменной с необычным именем "@#$\n" (содержащим символ перевода строки) и присвоению ей значения 254.

REB предусматривает так-же и параметрические строки, которые должны быть заключены в двойные кавычки. Использование параметрических строк аналогично их использованию в Perl и некоторых других скриптах.

Хэш

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

x:Aval 12;
x:`Bval` 18;
x:`C Value Hash`:z 24;
x:`C Value Hash`:v 25;

что приведет к превращению переменной x в хэш с тремя ключами: Aval, Bval и `C Value Hash`, причем последний сам является хэшем с двумя входами - z и v, которым присвоены значения 24 b 25.

Другой способ задать хэш - использовать встроенную функцию "hash", имеющую синонимы ".{" и "object". Тогда предыдущий пример перепишется в виде:

x hash {

	Aval 12;
	:`Bval` 18;
	:`C Value Hash`:z 24;
	:`C Value Hash`:v 25;
};

Ключ хэша можно задать и вызовом функции. Например, пусть вызов функции aaa(12) возвращает строку "Aval". Тогда к соответствующему входу хэша x можно обратиться следующим образом: x:{aaa(12)}. В этом случае (в отличие от вызова функции) фигурные скобки обязательны.

Правила работы с массивами практически такие же, за исключением того, что вместо фигурной скобки используется квадратная. Для задания массива можно использовать встроенную функцию array, имеющую синоним ".[":

y array 12, 29, "hi!";

что приведет к созданию массива y, проинициализированного значениями 12, 29 и "hi!". Нумерация ведется с нуля, поэтому обращение y:0 возвратит в этом случае 12 (как и y:[0]). Закрывая тему хэшей и массивов, скажу, что любая переменная, являющаяся хэшем, может одновременно являться и массивом, и наоборот.

Ссылки

В настоящее время REB предусматривает два вида ссылок: синонимы, то есть символические ссылки, не подлежащие разыменованию, и просто ссылки, которые можно разыменовать, и на которые тоже можно ссылаться. Пусть, например, описаны следующие переменные:

x:Aval 12;
px &x; //ссылка на x
y:ppx &px; //ссылка на px внутри хэша y
name x, xnm; //xnm - символическая ссылка на переменную x

Тогда к ключу Aval переменной x можно обратиться следующими способами:

(*px):Aval
[px]:Aval
[*y:ppx]:Aval
(**y:ppx):Aval
xnm:Aval

Ссылаться обоими способами можно на любые переменные, в том числе и на функции.

Объекты

Как и в Perl, поддержка объектной ориентированности в REB осуществляется с помощью хэша. Но отличия есть, и они делают поддержку ООП в REB более естественной. Каждый хэш в REB имеет предопределенный ключ "self", являющийся символической ссылкой на сам хэш. Кроме того, если функция является членом хэша, то при ее вызове к области глобальной видимости автоматически присоединяется и весь хэш, которому она принадлежит. Приведу пример:

//функция with делает примерно то-же самое, что и hash, но имеет несколько
//другую форму записи:
with MyClass, do {
    count 5; //член класса
    str ` `; //член класса

    //конструктор класса
    function construct, . {
        arg _str; //единственный аргумент конструктора
    // - новое значение для str
        copy self, b; //b - новый экземпляр класса MyClass
        b:str _str; //присваиваем str новое значение
        return b;
    };

    //а вот и функция - член класса
    function mprint, . {
        i count; //обращаемся к count как к члену класса
        while .>(i, 0), do {
            print str, "\n"; //обращаемся к str как к члену класса
            inc i, -1;
        };
    };
};

//конструируем новый объект obj:
obj MyClass:construct `OK!`;

//Теперь вызываем функцию-член:
obj:myprint;

Теперь посмотрим как осуществляется наследование в REB. Для создания класса-потомка необходимо сделать лишь следующее:

copy MyClass, newMyClass;

Теперь можно дополнить описание класса newMyClass:

with newMyClass, do {
    ...
};

Заметим, что все функции-члены являются виртуальными, как и в Perl. Для использования старого варианта функции предка его необходимо переименовать внутри нового класса:

with newMyClass, do {
    name mprint, old_mprint;
    function mprint, . {
        print "It's newMyClass:print";
        old_mprint;
    };
};

Заключение

В заключение скажу, что работа над REB только начинается, я развиваю его под лизензией GNU и приглашаю к сотрудничеству всех заинтересованных разработчиков. Первый ознакомительный релиз REB (для Cygwin) и исходные тексты доступны интернете здесь и здесь.

Кое что из описанного в данной статье будет изменено и очень многое будет дополнено. Я не описал здесь многих возможных и очень интересных применений REB - например, как универсального ассемблера.

Разработка языка ведется на С++.


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