ChaiScript — встраиваем скриптовый язык за пять минут.

Проблема автоматизации работы со сложными программами, такими как текстовые и графические редакторы, программы для построения графиков и визуализации данных, CAD-системами и т.п., возникла еще в незапамятные времена. Столь же давно было найдено и оптимальное решение — встраивание в программу специального скриптового языка, из которого можно вызывать все те функции, которые обычно выполняются пользователем интерактивно. Этот подход быстро эволюционировал из банального средства автоматизации в новую философию написания сложных программных систем. Оказалось, что можно создать быстрое и функциональное «ядро» на компилируемом языке (обычно на С или С++) и позволить вызывать его функции из скриптов. Скриптовый язык на порядок проще в освоении и скорости разработки, так что все «плюшки» и «навороты», которые не особенно требовательны к скорости выполнения и расходу памяти, можно писать уже на нем.

В результате, программа становится гибридом: ядро написано на одном языке, а большинство функций «высокого уровня» — на другом. Именно так написаны практически все современные игры — игровой движок «обвязан» скриптами, которые описывают миссии и компании, отвечают за «интеллект» персонажей и т.п. Без этой технологии не было бы «модов» и «extension packs», которые продлевают играм жизнь и вдохновляют множество энтузиастов. В «гибридном» подходе есть один очень серьезный изъян: полноценное встраивание скриптового языка обычно требует больших усилий и затрат времени. Это могут позволить себе крупные компании, но у одинокого разработчика или небольшого коллектива вскоре начинают опускаться руки. Чтобы разобраться, почему так получается, сначала определимся, какой язык лучше всего встраивать. Время «самопальных» языков, сделанных на коленке специально под конкретную программу, давно прошло, и сейчас обычно встраивают Python, Lua, JavaScript и изредка Tcl. Все эти языки вполне самостоятельны — у них свои типы и структуры данных, свои правила создания переменных и «уборки мусора», и все это совершенно не соответствует правилам С или C++. Каждую функцию «ядра» программы приходится помещать в «обертку», которая будет преобразовывать данные нужным образом. Это очень нудная, рутинная и чреватая ошибками работа. Для ее автоматизации существуют специальные генераторы привязок, такие как SWIG (http://www.swig.org/), Boost.Python (http://www.boost.org/doc/ libs/1 49 0/libs/python/doc) или LuaBind (http://www.rasterbar.com/products/luabind.html). Однако и они не позволяют полностью автоматизировать процесс. Все идет хорошо, пока в коде не встретится какой-то «экзотический» объект, семантику которого генератор не понимает и для которого опять приходится писать обертки вручную.

Кроме того, от «оберток» сильно страдает быстродействие и растет расход памяти. Скажем, список в Python несовместим с std::list в C++. Напрямую обратиться из скрипта к данным списка C++ нельзя — их надо сначала скопировать в список Python, а потом скопировать изменения назад.

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

Проект ChaiScript

ChaiScript (http://www.chaiscript.com) — это первый и единственный встраиваемый скриптовый язык, изначально спроектированный для полной совместимости с C++. Проект ChaiScript появился в 2009 г. как любительская разработка программиста Джейсона Тёрнера (Jason Turner). За время своего существования он достиг версии 3.1 и имеет на сегодняшний день трех активных разработчиков. Название языка выбрано с долей юмора — в нем обыгрывается чай (в отличие от Java и JavaScript, которые ассоциируются с кофе).

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

Все, что может понадобиться пользователю в скрипте, должно быть реализовано на С++ и зарегистрировано в интерпретаторе. Важно понимать, что ChaiScript нельзя использовать без «материнской» программы на С++ (в отличие от Python, Ruby или Lua). Сила ChaiScript кроется именно в необычайно легком встраивании и прозрачном взаимодействии с С++. ChaiScript распространяется по лицензии BSD, что позволяет использовать его как в свободных, так и в коммерческих проектах. Он является заголовочной (header-only) библиотекой для С++, поэтому работает на всех платформах, где есть современный компилятор С++. ChaiScript не добавляет к проекту никаких внешних зависимостей кроме динамического компоновщика dl и опционально библиотеки Boost.threads. Встроенный интерпретатор компактен, не раздувает размер исполняемого файла и потребляет очень немного памяти — идеальное сочетание для легкого встраиваемого языка. ChaiScript очень далек от «мэйнстрима». Мне удалось найти в сети всего три примера использования ChaiScript, и все они являются по сути «пробами пера». В больших проектах этот язык не используется вовсе. Тем не менее, ChaiScript чрезвычайно интересен в идейном плане и, на мой взгляд, является одним из тех прекрасных открытых проектов, которые совершенно незаслуженно остаются незамеченными.

Документация к проекту не настолько подробна, как хотелось бы. На странице http://www. chaiscript.com/doxygen/index.html сосредоточена вся имеющаяся информация по внедрению ChaiScript в программы на С++. Там же есть ссылки на встроенные функции самого языка, его формальный синтаксис (разобраться в «академической» EBNF-грамматике не так-то просто) и объектную модель. Документация в целом слишком лаконична и не описывает всех нюансов. Полноценного руководства по языку нет. Много полезного можно почерпнуть из форума на официальном сайте и примеров кода, которые идут в составе библиотеки.
Инсталлируем тестовый интерпретатор

СhaiScript — заголовочная библиотека, которая не требует установки как таковой, однако в поставке идет тестовый интерпретатор, который позволяет попробовать язык в интерактивном режиме, прежде чем приступать к его встраиванию в программы.

Сам СhaiScript не имеет никаких внешних зависимостей кроме динамического компоновщика dl (который есть в любой POSIX-совместимой системе) и библиотеки Boost.threads (она не нужна, если вы не планируете вызывать интерпретатор СhaiScript из нескольких потоков одновременно). Тестовый интерпретатор дополнительно требует наличия библиотеки readline (он может работать и без нее, но без возможности редактирования командной строки, что крайне неудобно).

Итак, скачаем последнюю версию ^aiScript с сайта https://github.com/ChaiScript/ChaiScript/ downloads в виде архива с исходными кодами и распакуем в удобное место. Далее нужно установить readline и Boost.threads если их нет в системе. В Ubuntu это делается командой:

$ sudo apt-get install libboost-thread-dev libreadline-dev

В директории с исходниками ChaiScript выполняем:

$ cmake .
$ Make

Если все прошло нормально, то появится программа chai — это и есть тестовый интерпретатор. Интерпретатор может работать интерактивно (по умолчанию), выполнять произвольную переданную ему строку кода (с ключом -с) или выполнять скрипт из файла, переданного как аргумент. Исходный код интерпретатора находится в файле src/main.cpp, и его очень удобно использовать как заготовку для собственных программ.
Приступаем к встраиванию

Чтобы показать, насколько удобен ChaiScript в качестве встраиваемого языка, создадим совершенно бесполезную гибридную программу. У нас будет один класс, который мы хотим сделать доступным в скрипте.

#include <chaiscript/chaiscript.hpp> using namespace chaiscript; using namespace std;
 
class Test { // Класс, который будет доступен в скрипте public:
 
// Конструкторы Test(double a){ data = a; }
 
Test(){ data = 0.0; }
 
void set_data(double a){ data = a; }
 
double get_data(){ return data; }
 
// Перегруженная функция 
double plus(double a){ return data+a; } double plus(int a){ return data+a; } private:
 
double data;
int main() {
 
// Инициализируем интерпретатор ChaiScript chai;
 
// Добавляем тип Test chai.add(user_type<Test>(), "Test");
 
// Добавляем конструкторы
 
chai.add(constructor<Test(double)>(), "Test"); chai.add(constructor<Test()>(), "Test");
 
// Конструктор копирования
 
chai.add(constructor<Test(const Test&)>(), "Test");
 
// Оператор присваивания chai.add(fun(&Test::operator=), "=");
 
// Добавляем методы
 
chai.add(fun(&Test::set_data),"set_data");
 
chai.add(fun(&Test::get_data),"get_data");
 
// Добавляем перегруженные методы
 
chai.add(fun<double(Test::*)(double)>(&Test::plus),"plus");
 
chai.add(fun<double(Test::*)(int)>(&Test::plus),"plus");
 
// Выполняем скрипт из файла chai.eval_file("test.chai"); return 0;
 
}

Вся функциональность ChaiScript включается единственным заголовком:

#include

Все функции «живут» в одноименном пространстве имен. В начале программы мы создаем и инициализируем интерпретатор ChaiScript строкой:

ChaiScript chai;

Он уже готов к работе: умеет выполнять действия с числами, строками и списками, может создавать объекты и функции, но пока «не видит» нашего класса Test. Метод интерпретатора add используется для добавления в него любых объектов: классов, функций и переменных. В начале мы сообщаем, что хотим добавить новый пользовательский тип:

chai.add(user_type(), «Test»);

Теперь интерпретатор знает, что все операции создания и копирования объектов типа Test в скрипте нужно переадресовывать соответствующим методам класса Test. Далее мы добавляем конструкторы нашего метода с указанием их сигнатур. Конструктора копирования и оператора присваивания в нашем классе в явном виде нет, но мы добавляем и их — интерпретатор, не задавая лишних вопросов, «подхватит» их автоматически сгенерированные варианты.
Обычные методы и перегруженные операторы добавляются с помощью служебной функции fun, в которую передается адрес нужного метода. Не нужно даже задавать их сигнатуры — интерпретатор никак их не проверяет до момента вызова. С перегруженными методами возникает проблема, поскольку непонятно, адрес какого именно из методов с одинаковыми названиями передается. Используя шаблонный вариант функции add, можно помочь компилятору добавив точную сигнатуру нужного метода. Синтаксис при этом получается несколько тяжеловесным и надо явно перечислить типы всех параметров, но иначе различить перегруженные методы нельзя.

Наконец, мы просим интерпретатор выполнить код на языке ChaiScript из файла test.chai. Этот файл может выглядеть так:

var t1 = Test() 
var t2 = Test(3.14) 
print(t1.get_data()) 
print(t2.get_data()) 
t2.set_data(42.0) 
t1 = t2
 
print(t1.get_data())

Скомпилируем нашу программу следующей командой:

$ g++ -1<путь к boost> -1<путь к ChaiScript> -DCHAISCRIPT_NO_THREADS chai.cpp -ldl

Если вы увидите множество ошибок типа «undefined reference to dlclose’», то вместо -ldl нужно явно указать путь к библиотеке libdl в виде <путь>/1^1^. У меня это потребовалось для 64-битной Ubuntu 11.10 с компилятором gcc 4.6.1 (причем clang++ 3.0 прекрасно видел libdl и без указания пути). Причины такого странного поведения остаются загадкой.

Ожидаемо на экран будет выведено:

0
3.14
42

author Семен Есилевский


http://blog.wel.org.ua

работаю админом, прогером сеошнегом :)

Comments to ChaiScript — встраиваем скриптовый язык за пять минут.

  • Спасибо за содержательную статью!
    Хотелось бы добавить, что важнейшим применением ChaiScript, собственно то, ради чего он и был создан (о чем автор не упоминает), является система и протокол Open Transactions, автоматизация с помощью скриптов финансовой криптографии.

    adru 15.03.2014 19:25 Ответить

Leave a Comment

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Загрузка...
Menu Title