ChaiScript — скриптовый язык для C++

от автора

Когда возникает потребность внедрить скриптовый язык в проект на C++, первым делом большинство людей вспоминает Lua. В этой статье его не будет, я расскажу о другом, не менее удобном и легком в освоении языке под названием ChaiScript.
image

Небольшое предисловие

Сам я наткнулся на ChaiScript случайно, когда смотрел один из докладов Jason’а Turner’a, одного из создателей языка. Меня это заинтересовало, и в тот момент, когда нужно было выбрать скриптовый язык в проект, я решил — почему бы не попробовать ChaiScript? Результат меня приятно удивил (о моем личном опыте будет написано ближе к концу статьи), однако, как бы странно это ни звучало, на хабре не оказалось ни одной статьи, в которой хоть как-то бы упоминался этот язык, и я решил, что было бы неплохо написать о нем. У языка конечно есть документация и официальный сайт, но из наблюдений далеко не каждый будет ее читать, да и формат статьи многим (включая меня) ближе.

Сначала мы поговорим о синтаксисе языка и всех его фичах, потом о том, как внедрить его в ваш проект C++, а в конце я расскажу немного о своем опыте. Если какая-то часть вас не интересует, или вы хотите прочитать статью в другом порядке, можете воспользоваться оглавлением:

Синтаксис языка

Язык ChaiScript очень похож на C++ и JS своим синтаксисом. Прежде всего, он, как и подавляющее большинство скриптовых языков, является динамически-типизированным, однако в отличие от JavaScript, имеет строгую типизацию (никаких 1 + "2"). Также есть встроенный сборщик мусора, язык является полностью интерпретируемым, позволяя исполнять код построчно, без компиляции в байткод. Имеет поддержку исключений (причем совместную, позволяя ловить их как внутри скрипта, так и в C++), лямбда-функции, перегрузку операторов. Не чувствителен к пробелам, позволяя писать как в одну строку через точку-с-запятой, так и в стиле python, разделяя выражения новой строкой.

Примитивные типы

ChaiScript по умолчанию хранит целочисленные переменные как int, вещественные как double, а строки с помощью std::string. Сделано это прежде всего для того, чтобы обеспечить совместимость с вызывающим кодом. В языке даже есть суффиксы у чисел, чтобы мы могли явно указать, какого типа является наша переменная:

/*  переменные в chaiscript объявляются как в js тип указывать не нужно, достаточно var / auto `;` в конце строки по желанию  */ var myInt = 1 // int var myLongLong = 1ll // long long int var myFloating = 3.3 // double var myBoolean = false // bool var myString = "hello world!\n" // std::string 

Менять тип переменных просто так не выйдет, скорее всего вам необходимо будет определить свой оператор `=` для этих типов, иначе вы рискуете либо вызвать исключение (об этом мы поговорим позже), либо стать жертвой округления, так:

var integer = 3 integer = 5.433 print(integer) // печатает 5 за счет округления double при присвоении в int! integer = true // вызывает исключение - не оператора `=` для (int, bool) 

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

Встроенные контейнеры

В языке предусмотрено два контейнера — Vector и Map. Работают они очень схоже с аналогами в C++ (std::vector и std::map соответственно), однако не требуют указания типа, потому что могут хранить любой. Индексировать Vector можно как обычно с помощью int’ов, а вот Map требует ключом обязательно строку. Видимо вдохновившись python, авторы также добавили возможность быстро объявлять контейнеры в коде с помощью следующего синтаксиса:

var v = [ 1, 2, 3u, 4ll, "16", `+` ] // массив с элементами разных типов var m = [ "key1" : 1, "key2":  "Bob" ]; // словарь с произвольными типами-значениями  var M = Map() // создает пустой словарь var V = Vector() // создает пустой массив  // в массив можно добавлять элементы в стиле C++ вектора: v.push_back(123)  // есть нужно добавить ссылку, можно воспользоваться отдельной функцией v.push_back_ref(m); // m - произвольный объект  // в словарь добавлять также легко m["key"] = 3  // по ссылке можно через ссылочное присваивание (reference assignment): m["key"] := m // теперь словарь хранит ссылку на объект  

Оба этих класса почти в полной мере повторяют свои аналоги в C++, за исключением итераторов, ведь вместо них существует специальные классы Range и Const_Range. К слову, все контейнеры передаются по ссылке даже если вы пользуетесь присваиванием через =, что для меня весьма странно, так как для всех остальных типов происходит копирование по значению.

Условные конструкции

Почти все конструкции условий и циклов можно описать буквально в одном примере кода:

var a = 5 var b = -1 // классический if-else if (a > b) {     print("a > b") } else if (a == b){     print("a == b") } else {     print("a < b") }  // switch - раскрывается как набор if-ов // Можно проверять переменную любого типа // Без break исполнение также проваливается вниз, как и в C++ var str = "hello" switch(str) {     case("hi") { print("hi!"); break; }     case("hello") { print("hello!" break; }     case("bye") { print("bye-bye!") break; }     default { print("what have you said?") } }  var x = true // фигурные скобки должны всегда присутствовать, даже если в теле одна строка while (x) {   print("x was true")   x = false; }  // цикл в стиле C. По желанию можно объявить одну переменную в начале, после идет булево выражение, в конце любое выражение, исполняющееся каждую итерацию for (var i = 0; i < 10; ++i) // есть только пре-инкеремент, значение увеличивается сразу {   print(i); // печатаем 0 ... 9 в 10 строк }  // ranged-for loop for(element : [1, 2, 3, 4, 5]) {     puts(element) // печатает подряд 12345 }  // для гурманов: прямиком из C++17 if-init statements: if(var x = get_value(); x < 10) {     print(x) // x является локальной переменной внутри if } 

Думаю, люди, знакомые с C++, нового ничего не нашли. Это не удивительно, ведь ChaiScript позиционируется, как легкий для «сишников» в освоении язык, и поэтому заимствует всем известные классические конструкции. Авторы решили выделить даже два ключевых слова для объявления переменных — var и auto, на случай, если вам очень уж сильно нравятся плюсы с auto.

Контекст выполнения

В ChaiScript есть локальный и глобальный контекст. Код исполняется сверху вниз построчно, однако его можно вынести в функции и вызвать позднее (но не раньше!). Переменные, объявленные внутри функций или условий/циклов по умолчанию не видны извне, но вы можете изменить это поведение, используя идентификатор global вместо var. Глобальные переменные отличаются от обычных тем, что, во-первых: видны за пределами локального контекста, а, во-вторых: могут быть заново объявлены (если при повторном объявлении не задается значение, то оно остается прежним)

// простая функция на языке chaiscript def foo(x) {     global G = 2     print(x) }  foo(0) // вызов foo(x), G = 2 print(G)  // печатает 2 global G = 3 // теперь G = 3, повторное объявление global - не ошибка! 

К слову, если у вас есть переменная, и нужно проверить, присвоено ли ей значение, воспользуйтесь встроенной функцией is_var_undef, возвращающей true, если переменная undefined.

Интерполяция строк

Базовые объекты или пользовательские, у которых определен метод to_string(), могут быть помещены в строку с помощью синтаксиса ${object}. Это позволяет избежать лишних конкатенаций строк и в целом выглядит намного опрятней:

var x = 3 var y = 4  // печатает sum of 3 + 4 = 7 print("sum of ${x} + ${y} = ${x + y}")  

Vector, Map, MapPair и все примитивы также поддерживают эту функцию. Vector выводится в формате [o1, o2, ...], Map как [<key1, val1>, <key2, val2>, ...], а MapPair: <key, val>.

Функции и их нюансы

Функции ChaiScript — такие же объекты, как и все остальное. Их можно захватывать, присваивать переменным, делать вложенными в другие функции и передавать как аргумент. Также для них вы можете указать тип входных значений (то чего так не хватало динамически-типизированным языкам!), для этого надо указать тип перед объявлением параметра функции. Если при вызове параметр можно преобразовать в указанный, то произойдет преобразование по правилам C++, иначе генерируется исключение:

def adder(int x, int y) {     return x + y }  def adder(bool x, bool y) {    return x || y }  adder(1, 2) // ок, результат 3 adder(1.22, -3.7) // ок, результат 1 + (-3) = 2 adder(true, true) // ок, результат true  adder(true, 3) // ошибка, нет подходящей функции adder(bool, int) 

Функциям в языке также можно задавать условия вызова (call guard). Если он не соблюдается, вызывается исключение, иначе выполняется вызов. Также отмечу, что если функция не имеет return-statement’а в конце, то вернется последнее выражение. Очень удобно для небольших подпрограмм:

def div(x, y) : y != 0 { x / y } // если `y` не равен нулю - вернуть результат деления `x` на `y`  print(div(2, 0.5)) // печатает 4.0 print(div(2, 0)) // ошибка, `y` равен 0! 

Классы и Dynamic_Object

ChaiScript имеет зачатки ООП, что является несомненным плюсом в случае, если вам необходимо манипулировать сложными объектами. В языке присутствует особый тип — Dynamic_Object. По факту все экземпляры классов и пространства имен являются именно Dynamic_Object с заранее заданными свойствами. Динамический объект позволяет добавлять к нему поля по ходу выполнения скрипта, а после обращаться к ним:

var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); } // теперь obj имеет метод f (внутри него можно обращаться к `x` obj.f(-3); // печатает 0 

Классы определяются достаточно просто. Им можно задать поля, методы, конструкторы. Из интересного — через специальную функцию set_explicit(object, value) можно «зафиксировать» поля объекта, запретив добавление новых методов или атрибутов после объявления класса (обычно это делается в конструкторе):

class Widget {     var id; // атрибут id     def Widget() { this.id= 0 } // конструктор без параметров     def Widget(id) { this.id = id } // конструктор с 1 параметров     def get_id() { id } // метод класса } var w = Widget(10) print(w.get_id()) // печатает 10 (w.id) print(w.get_id) // также печатает 10, скобки могут быть опущены если нет параметров set_explicit(w, true) // зафиксировать объект класса w.x = 3 // вызовет ошибку так как у Widget нет поля x 

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

def set_id(Widget w, id) { w.id = id }  w.set_id(9) // w.id = 9 set_id(w, 9) // тоже самое, w.id = 9 

Кто знаком с C#, может заменить, что больно похоже это на метод расширения, и будет недалек от истины. Таким образом, в языке вы можете добавить новый функционал даже для встроенных классов, к примеру для строки или int’а. Также авторы предлагают хитрый способ перегрузки операторов: чтобы его сделать, необходимо окружить символ оператора тильдой (`) как в примере ниже:

// перегрузка оператора + для двух объектов типа Widget def `+`(Widget w1, Widget w2) {     print("merging two widgets!") }  var widget1 = Widget() var widget2 = Widget() widget1 + widget2 // без функции выше генерируется исключение  // оператор также можно захватить как функцию и вызвать: var plus = `+` print(plus(1, 7)) // печатает 8 

Пространства имен

Говоря о пространстве имен в ChaiScript, стоит иметь в виду, что это по существу классы, которые всегда находятся в глобальном контексте. Вы можете создать их с помощью функции namespace(name), а после добавить необходимые функции и классы. По умолчанию в языке нет никаких библиотек, однако вы можете их установить с помощью расширений, о которых мы поговорим чуть позже. В целом, инициализация пространства имен может выглядеть так:

namespace("math") // создаем пространство имен math  // добавим функций math.square = fun(x) { x * x } math.hypot_squared= fun(x, y) { math.square(x) + math.square(y) }  print(math.square(4)) // печатает 16 print(math.hypot_squared(3, 4)) // печатает 25 

Лямбда выражения и другие фичи

Лямбда выражения в ChaiScript подобны тем, что мы знаем из C++. Для них используется ключевое слово fun, и они также требуют явного указания захватываемых переменных, однако делают это всегда по ссылке. Также в языке есть функция bind, которая позволяет привязать значения к параметрам функции:

var func_object = fun(x) { x * x } func_object(9) // печатает 81  var name = "John" var greet = fun[name]() { "Hello, " + name }  print(greet()) // печатает Hello, John name = "Bob" print(greet()) // печатает Hello, Bob  var message = bind(fun(msg, name) { msg + " from " + name }, _, "ChaiScript"); print(message("Hello")) // печатает Hello from ChaiScript 

Исключения

Во время выполнения скрипта могут возникнуть исключения. Они могут быть как перехвачены в самом ChaiScript (что мы здесь и обсудим), так и в C++. Синтаксис абсолютно идентичен с плюсами, вы можете даже выкидывать число или строку:

try {     eval(x + 1) // x не существует } catch (e) {     print("Error during evaluation")) }  // можно ловить C++ исключения внутри ChaiScript // Так как Vector - обертка над std::vector, то он кидает std::exception при обращении за пределы массива try {     var vec = [1, 2]     var val = vec[3] // обращение за пределы массива } catch (e) {     print("index out of range: " + e.what()); // e.what преобразуется в строку ChaiScript }  // к сatch можно добавить guard так же как для функций, проверяется условие после `:` try {   throw(5.2) } catch(e) : is_type(e, "int") {   print("Int: ${e}"); // только если `e` это int } catch(e) : is_type(e, "double") {     print("Double: ${e}"); // если `e` это double } 

По-хорошему, вы должны определить свой класс исключений и кидать его. О том, как его перехватывать в C++, мы поговорим во втором разделе. Для исключений интерпретатора ChaiScript генерирует свои исключения, такие как eval_error, bad_boxed_cast и т.п.

Константы интерпретатора

К моему удивлению в языке оказалось и некое подобие макросов компилятора — их всего 4 и все они служат для выявления контекста и по большей части используются для обработки ошибок:

__LINE__ текущая строка, если код исполняется не из файла, то ‘1’
__FILE__ текущий файл, если код вызывается не из файла, то «__EVAL__»
__CLASS__ текущий класс или «NOT_IN_CLASS»
__FUNC__ текущая функция или «NOT_IN_FUNCTION»

Перехват ошибок

Если функция, которую вы вызываете, не была объявлена, вызывается исключение. Если для вас это неприемлемо, вы можете определить специальную функцию — method_missing(object, func_name, params), которая будет вызвана с соответствующими аргументами в случае ошибки:

def method_missing(Widget w, string name, Vector v)  {     print("widget method ${name} with params {v} was not found") }  w = Widget() w.invoke_error(1, 2, 3) // печатает widget method invoke_error with params [1, 2, 3] was not found 

Built-in функции

В ChaiScript определяется множество встроенных функций, и в статье хотелось бы рассказать об особо полезных. Среди них: eval(str), eval_file(filename), to_json(object), from_json(str):

var x = 3 var y = 5 var res = eval("x * y") // res = 15, в eval используется контекст вызывающего кода  // можем запустить скрипт из файла: // через eval_file eval_file("source.chai") // или через use, последний гарантирует, что повторный вызов с тем же файлом игнорируется use("source.chai")  // to_json превращает объект в Map и возвращает строку var w = Widget(0) var j = to_json(w) // j = "{ "id" : 0 }" // from_json преобразует строку в Map (к сожалению, не в объект) var m = from_json(" {     "x": 0,     "y": 3,     "z": 2 }") print(m) // печатает Map как [<x, 0>, <y, 3>, <z, 2>] 

Внедрение в C++

Установка

ChaiScript является header-only библиотекой C++, построенной на шаблонах. Соответственно для установки вам всего лишь нужно сделать clone репозитория или просто поместить все файлы из этой папки в ваш проект. Так как в зависимости от IDE все это делается по-разному и уже давно детально расписано на форумах, далее будем предполагать, что у вас получилось подключить библиотеку, и код с include-ом: #include <chaiscript/chaiscript.hpp> компилируется.

Вызов кода C++ и загрузка скрипта

Минимальный пример кода с использованием ChaiScript выглядит как показано ниже. Мы определяем простую функцию в C++, принимающую std::string и возвращающую измененную строку, а затем добавляем ссылку на нее в объект ChaiScript, чтобы в нем вызвать. Компиляция может занять значительное время, но связано это прежде всего с тем, что инстанцирование большого количества шаблонов для компилятора дело не из легких:

#include <string> #include <chaiscript/chaiscript.hpp>  std::string greet_name(const std::string& name)  {     return "hello, " + name; }  int main()  {     chaiscript::ChaiScript chai; // объект chaiscript     chai.add(chaiscript::fun(&greet_name), "greet"); // добавляем функцию как greet      // мгновенный eval  с выводом результата в консоль     chai.eval(R"(         print(greet("John"));     )"); }

Надеюсь у вас получилось, и вы увидели результат выполнения функции. Сразу хочу отметить один нюанс — если вы объявите объект ChaiScript как статический, то получите неприятную ошибку времени выполнения. Связано это с тем, что язык по умолчанию поддерживает многопоточность и хранит локальные переменные потока, к которым обращается в своем деструкторе. Однако, уничтожаются они раньше, чем вызывается деструктор статического экземпляра, и в итоге мы имеем ошибку access violation или segmentation fault. Исходя из issue на github, самым простым решением будет просто поставить #define CHAISCRIPT_NO_THREADS в настройках компилятора или перед включением файла библиотеки, тем самым отключив многопоточность. Как я понял, пофиксить эту ошибку так и не удалось.

Теперь разберем детально, как же происходит взаимодействие С++ и ChaiScript. В библиотеке определена специальная шаблонная функция fun, которая может принимать указатель на функцию, функтор или указатель на переменную класса, а затем возвращать специальный объект, хранящий состояние. В качестве примера определим в C++ коде класс Widget и попробуем по-разному связать его с ChaiScript:

class Widget {     int Id; public:     Widget(int id) : Id(id) { }     int GetId() const { return this->Id; } };  std::string ToString(const Widget& w) {     return "widget #" + std::to_string(w.GetId()); }  int main() {     chaiscript::ChaiScript chai;     Widget w(2); // создадим Widget в C++ коде      chai.add(chaiscript::fun([&w] { return w; }), "get_widget"); // захватим его в лямбду и передадим в скрипт     chai.add(chaiscript::fun(ToString), "to_string"); // внешняя функция     chai.add(chaiscript::fun(&Widget::GetId), "get_id"); // метод класса      // для примера вызовем код, который получает Widget и вызывает сначала GetId, а затем неявно to_string, который мы перегрузили     chai.eval(R"(         var w = get_widget()         print(w.get_id) // печатает 2         print(w) // печатает widget #2         )"); } 

Как видите, ChaiScript абсолютно спокойно работает с неизвестными ему классами C++ и умеет вызывать их методы. Если же вы где-то в коде ошибетесь, скорее всего скрипт выкинет исключение рода error in function dispatch, что совсем не критично. Однако не только функции можно импортировать, давайте посмотрим, как добавить переменную в скрипт средствами библиотеки. Для этого выберем задачу чуть потруднее — импортировать std::vector<Widget>. В этом нам помогут функция chaiscript::var и метод add_global. Также добавим public-поле Data в наш Widget, чтобы посмотреть, как импортировать поле класса:

class Widget {     int Id; public:     int Data = 0;     Widget(int id) noexcept : Id(id) { }     int GetId() const { return this->Id; } };  std::string ToString(const Widget& w) {     return "widget #" + std::to_string(w.GetId()) +         " with data: " + std::to_string(w.Data);  int main() {     chaiscript::ChaiScript chai;      std::vector<Widget> W; // зададим массив из Widget     W.emplace_back(1);     W.emplace_back(2);     W.emplace_back(3);      chai.add(chaiscript::fund(ToString), "to_string");      chai.add(chaiscript::fun(&Widget::Data), "data"); // указатель на поле класса           // добавим глобальный объект в ChaiScript     chai.add_global(chaiscript::var(std::ref(W)), "widgets"); // избегаем копирования с помощью std::ref     chai.add(chaiscript::fun(&std::vector<Widget>::size), "size"); // размер массива         // индексация. Пояснение к этим двум строкам дано ниже     using IndexFuncType = Widget& (std::vector<Widget>::*)(const size_t);     chai.add(chaiscript::fun(IndexFuncType(&std::vector<Widget>::operator[])), "[]");      chai.eval(R"(         for(var i = 0; i < vec.size; ++i)         {             vec[i].data = i * 2;             print(vec[i])         }     )"); } 

Код выше выводит на экран: widget #1 with data: 0, widget #2 with data: 2, widget #3 with data: 4. Мы добавили в ChaiScript указатель на поле класса, и так как поле оказалось примитивным типом, мы изменить его значение. Также для работы с std::vector было добавлено несколько его методов, среди которых — operator[]. Те, кто хорошо знакомы с STL, знают, что метода индексации у std::vector два — один возвращает константную ссылку, другой — простую ссылку. Именно поэтому для перегруженных функций нужно явно указывать их тип — иначе возникает неоднозначность, и компилятор выдаст ошибку.

Библиотека предоставляет еще несколько методов для добавления объектов, но все они практически идентичны, поэтому рассматривать их подробно не вижу смысла. В качестве небольшой подсказки приведу код ниже:

chai.add(chaiscript::var(x), "x"); // x копируется в ChaiScript  chai.add(chaiscript::var(std::ref(x), "x"); // по ссылке, можно изменять из C++ и ChaiScript  auto shared_x = std::make_shared<int>(5); chai.add(chaiscript::var(shared_x), "x"); // shared_ptr существует пока есть указатели в C++ или ChaiScript  chai.add(chaiscript::const_var(x), "x"); //  копируется в ChaiScript и становится константой  chai.add_global_const(chaiscript::const_var(x), "x"); // global const переменная. Исключение, если x уже существует  chai.add_global(chaiscript::var(x), "x"); // global переменная, Исключение. если x уже существует  chai.set_global(chaiscript::var(x), "x"); // устанавливает значение global переменной, если та не const 

Использование STL контейнеров

Если вы хотите передать STL-контейнеры, содержащие примитивные типы в ChaiScript, вы можете добавить инстанцирование шаблонного контейнера в ваш скрипт, чтобы не импортировать для каждого типа его методы.

using MyVector = std::vector<std::pair<int, std::string>>;  MyVector V; V.emplace_back(1, "John"); V.emplace_back(3, "Bob");  // добавим наши типы - vector и pair chai.add(chaiscript::bootstrap::standard_library::vector_type<MyVector>("MyVec")); chai.add(chaiscript::bootstrap::standard_library::pair_type<MyVector::value_type>("MyVecData"));  chai.add(chaiscript::var(std::ref(V)), "vec"); chai.eval(R"(     for(var i = 0; i < vec.size; ++i)     {       print(to_string(vec[i].first) + " " + vec[i].second)     }   )");

Под капотом идет вызов нескольких функций ChaiScript, которые сами добавляют необходимые методы. В целом, если ваш класс поддерживает схожие операции с STL-контейнерами, вы также можете добавить его таким способом. В случае c std::vector<Widget> это, к сожалению, невозможно, так как ChaiScript требует наличие конструктора без параметров для элемента vector_type, коего у нашего Widget не было.

С++ классы внутри ChaiScript

Возможно в рамках вашей задачи необходимо не только изменять объекты в ChaiScript, но и создавать их в скрипте. Что же, это вполне возможно. Возьмем снова для примера класс Widget и унаследуем от него класс WindowWidget, а затем добавим в скрипт возможность создавать их оба, а также конвертировать унаследованный класс в базовый:

class Widget {     int Id; public:     Widget(int id) : Id(id) { }     int GetId() const { return this->Id; } };  class WindowWidget : public Widget {     std::pair<int, int> Size; public:     WindowWidget(int id, int width, int height)         : Widget(id), Size(width, height) { }      int GetWidth()  const { return this->Size.first; }     int GetHeight() const { return this->Size.second; } };  int main() {     chaiscript::ChaiScript chai;      // добавим тип Widget и его конструктор     chai.add(chaiscript::user_type<Widget>(), "Widget");     chai.add(chaiscript::constructor<Widget(int)>(), "Widget");      // добавим тип WindowWidget и его конструктор     chai.add(chaiscript::user_type<WindowWidget>(), "WindowWidget");     chai.add(chaiscript::constructor<WindowWidget(int, int, int)>(), "WindowWidget");      // скажем, что Widget - базовый класс для WindowWidget     chai.add(chaiscript::base_class<Widget, WindowWidget>());      // добавим методы Widget и WindowWidget     chai.add(chaiscript::fun(&Widget::GetId), "get_id");      chai.add(chaiscript::fun(&WindowWidget::GetWidth), "width");     chai.add(chaiscript::fun(&WindowWidget::GetHeight), "height");      // создадим WindowWidget и вызовем его методы     chai.eval(R"(         var window = WindowWidget(1, 800, 600)         print("${window.width} * ${window.height}")         print("widget.id is ${window.get_id}")     )"); } 

Полиморфизм работает в ChaiScript абсолютно также, как и в C++ для типов, информацию о которых вы предоставили. Если по каким-то причинам возникает неоднозначность при добавлении указателя на унаследованный метод (возможно, класс наследуется сразу от нескольких базовых), приведите его к нужному классу явно, как это сделано в примере выше с оператором индексации std::vector<Widget>.

Привязка экземпляра к методу и конвертирование типа

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

Widget w(3); w.Data = 4444;  // привязка Widget w chai.add(chaiscript::fun(&Widget::GetId, &w), "widget_id"); chai.add(chaiscript::fun(&Widget::Data, &w), "widget_data");  chai.eval(R"(     print(widget_id)     print(widget_data) )"); 

Также при экспорте более «библиотечных» классов из C++ в ChaiScript (к примеру, vec3, complex, matrix) часто требуется возможность неявного преобразования из одного типа в другой. В ChaiScript эта задача решается путем добавления type_conversion в объект скрипта. Для примера рассмотрим класс Complex и реализацию преобразования int и double в него при сложении:

class Complex { public:     float Re, Im;     Complex(float re, float im = 0.0f) : Re(re), Im(im) { } };  int main() {     chaiscript::ChaiScript chai;      // добавим Complex, определив поля re, im, конструктор и оператор `=`     chai.add(chaiscript::user_type<Complex>(), "Complex");     chai.add(chaiscript::bootstrap::standard_library::assignable_type<Complex>("Complex"));     chai.add(chaiscript::constructor<Complex(float, float)>(), "Complex");     chai.add(chaiscript::fun(&Complex::Re), "re");     chai.add(chaiscript::fun(&Complex::Im), "im");      // добавим неявное преобразование из double и int в Complex     chai.add(chaiscript::type_conversion<int, Complex>());     chai.add(chaiscript::type_conversion<double, Complex>());      // в скрипте определим оператор `+` для произвольного типа     chai.eval(R"(         def `+`(Complex c, x)         {             var res = Complex(0, 0)             res.re = c.re + x.re             res.im = c.im + x.im             return res         }          var c = Complex(1, 2)         c = c + 3         print("${c.re} + ${c.im}i")     )"); // результат: `4 + 2i` } 

Таким образом, не обязательно писать функцию преобразования в самом C++, а лишь затем экспортировать ее в ChaiScript. Можно добавить преобразования, и уже в самом коде скрипта описать новый функционал. Если же конвертация для двух типов нетривиальна, вы можете передать лямбду аргументом в функцию type_conversion. Она будет вызываться при приведении типов.
Схожий принцип используется для преобразования Vector или Map ChaiScript’а в ваш пользовательский тип. Для этого в библиотеке определены vector_conversion и map_conversion.

Распаковка возвращаемых значений ChaiScript

Методы eval и eval_file возвращают значение последнего выполненного выражения в виде объекта Boxed_Value. Чтобы распаковать его и использовать результат в коде C++, вы можете как явно указать тип возвращаемого значения, так и использовать функцию boxed_cast<T>. Если преобразование между типами существует, оно будет выполнено, иначе возникнет исключение bad_boxed_cast:

// сразу привести результат к нужному типу double d = chai.eval<double>("5.3 + 2.1");  // сохранить результат в виде Boxed_Value, затем привести к типу auto v = chai.eval("5.3 + 2.1"); double d = chai.boxed_cast<double>(v); 

Так как все объекты внутри ChaiScript хранятся с помощью shared_ptr, вы можете получить объект в виде указателя для последующей работы с ним. Для этого явно укажите тип shared_ptr при конвертации возвращаемого значения:

auto x = chai.eval<std::shared_ptr<double>>("var x = 3.2"); 

Главное, не стоит хранить ссылку на значение разыменнованного shared_ptr, иначе вы рискуете получить access violation после того, как переменная будет удалена в ходе автоматической сборки мусора в скрипте.

Так же как и переменные, вы можете достать из ChaiScript функции в виде запакованных функторов, захватывающих состояние объекта ChaiScript. Для примера, воспользуемся уже реализованным функционалом класса Complex и попробуем с помощью него вызвать функцию на этапе выполнения программы:

auto printComplex = chai.eval<std::function<void(Complex)>>(R"(     fun(Complex c) { print("${c.re} + ${c.im}i"); } )"); // создаем лямбду, печатающую объект класса, а затем возвращаем ее в C++ printComplex(Complex(2, 3)); // вызов chaiscript, печатает `2 + 3i` 

Перехват исключений из ChaiScript

Авторы рекомендуют ловить три типа исключений помимо тех, которые вы генерируется самостоятельно. Это eval_error для ошибок времени выполнения, bad_boxed_cast, вызывающийся при неправильной распаковке возвращаемых значений и std::exception для всего остального. Если же вы планируете выкидывать свои собственные исключения, вы можете настроить автоматическое преобразование в типы С++:

class MyException : public std::exception { public:     int Data;     MyException(int data) : std::exception("MyException"), Data(data) { } };  int main() {     chaiscript::ChaiScript chai;          // добавим исключение как тип в chaiscript     chai.add(chaiscript::user_type<MyException>(), "MyException");     chai.add(chaiscript::constructor<MyException(int)>(), "MyException");      try     {         // укажем к каким типам пытаться приводить исключения внутри скрипта         chai.eval("throw(MyException(11111))",             chaiscript::exception_specification<MyException, std::exception>());     }     catch (MyException& e)     {         std::cerr << e.Data; // здесь выведется `11111`     }     catch (chaiscript::exception::eval_error& e)     {         std::cerr << e.pretty_print();     }     catch(std::exception& e)     {         std::cerr << e.what();     } } 

В примере выше показано, как можно ловить большинство исключений в C++. Помимо метода pretty_print, у eval_error имеется еще много полезных данных, таких как стек вызовов, имя файла, детали ошибки, но так сильно углубляться в этот класс в рамках этой статьи мы не будем.

Библиотеки ChaiScript

К сожалению, по умолчанию ChaiScript не предоставляет дополнительного функционала в плане библиотек. К примеру, в нем отсутствуют математические функции, хеш-таблицы, большинство алгоритмов. Часть из них вы можете скачать в виде библиотек-модулей из официального репозитория ChaiScript Extras, а затем импортировать в ваш скрипт. Для примера возьмем библиотеку math и функцию acos(x):

#include <chaiscript/chaiscript.hpp> #include <chaiscript/extras/math.hpp>  int main() {     chaiscript::ChaiScript chai;     // добавим библиотеку     auto mathlib = chaiscript::extras::math::bootstrap();     chai.add(mathlib);      std::cout << chai.eval<double>("acos(0.5)"); // ~1.047 } 

Вы также можете написать свою библиотеку для языка и потом импортировать. Делается это достаточно просто, поэтому советую ознакомиться с открытым кодом math или другой либы в репозиторие. В принципе, в рамках интеграции с C++ мы рассмотрели почти все, так что считаю, что раздел на этом можно закончить.

Личный опыт

В данный момент я пишу 3D-движок под OpenGL в качестве персонального проекта, и у меня возникла вполне закономерная идея реализовать консоль отладки, чтобы в реальном времени управлять состоянием приложения посредством команд. Можно было бы конечно заняться велосипедированием, но, как говорится, «игра бы не стоила свеч», поэтому я решил взять готовую библиотеку.
Как я упомянул в начале статьи, о ChaiScript к тому моменту я уже знал, поэтому передо мной стоял выбор между ним и Lua. До того момента ни с тем, ни с другим языком я знаком не был, поэтому больше всего влияли такие факторы как: понятность синтаксиса, легкость внедрения в уже существующий код и поддержка C++ вместо C, чтобы не городить забор из ООП-оберток над C-style функциями. Думаю, по ходу чтения данной статьи вы уже догадались, на что пал мой выбор.

На данный момент язык меня более чем устраивает, и писать обвязки над классами не составляет большого труда. В коде движка к запускаемому приложению прикреплен один экземпляр консоли на ImGui, в которой инициализируется объект chaiscript. Парой макросов задача о внедрении нового класса в скрипт сводится к простому описанию всех методов, которые необходимо экспортировать:

// фрагмент кода с инициализацией методов 3D-объекта:  // rotation CHAI_IMPORT(&GLInstance::RotateX, rotate_x); CHAI_IMPORT(&GLInstance::RotateY, rotate_y); CHAI_IMPORT(&GLInstance::RotateZ, rotate_z);  // scale CHAI_IMPORT((GLInstance&(GLInstance::*)(float))&GLInstance::Scale, scale); CHAI_IMPORT((GLInstance&(GLInstance::*)(float, float, float))&GLInstance::Scale, scale);  // translation CHAI_IMPORT(&GLInstance::Translate, translate); CHAI_IMPORT(&GLInstance::TranslateX, translate_x); CHAI_IMPORT(&GLInstance::TranslateY, translate_y); CHAI_IMPORT(&GLInstance::TranslateZ, translate_z); 		 // hide / show CHAI_IMPORT(&GLInstance::Hide, hide); CHAI_IMPORT(&GLInstance::Show, show);  // getters CHAI_IMPORT(&GLInstance::GetTranslation, translation); CHAI_IMPORT(&GLInstance::GetRotation, rotation); CHAI_IMPORT(&GLInstance::GetScale, scale); 

Таким же образом экспортируются еще несколько классов, а затем все соединяется вместе лямбда-функциями, объявленными прямо в коде инициализации. Результат работы скрипта вы можете увидеть на скриншоте:
image
консоль с chaiscript на ImGui: загрузка и установка объекта через команды

Учитывая в целом гибкость библиотеки, поменять подход к экспорту классов в скрипт не составит практически никакого труда. Безусловно, Lua обладает более обширной документацией и сообществом, и этот язык будет предпочтительней в случае, если вам нужно получить большую производительность от кода скрипта (JIT все же делает свое дело), но списывать со счетов ChaiScript все же не стоит. Если у вас есть небольшой проект, который нуждается в скриптинге, можете смело эксперементировать с доступными альтернативами.

На этой ноте я бы хотел завершить данную статью. Если вы уже имели опыт работы со скриптовыми языками внутри C++ (будь то Lua или другой язык), в комментариях буду рад услышать ваше мнение о ChaiScript и скриптингу в целом. Также я приветствую любые вопросы или замечания касательно публикации. Всем спасибо за прочтение.

Полезные ссылки


ссылка на оригинал статьи https://habr.com/ru/post/483540/


Комментарии

Добавить комментарий

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