ChaiScript - linguagem de script para C ++

Quando é necessário incorporar uma linguagem de script em um projeto C ++, a primeira coisa que a maioria das pessoas lembra é Lua. Neste artigo não será, vou falar sobre outro, não menos conveniente e fácil de aprender linguagem chamada ChaiScript.

imagem

Breve introdução


Eu mesmo me deparei com o ChaiScript por acaso, quando assisti a uma das palestras de Jason Turner, um dos criadores da linguagem. Isso me interessou e, naquele momento em que era necessário escolher uma linguagem de script no projeto, eu decidi - por que não experimentar o ChaiScript? O resultado me surpreendeu agradavelmente (minha experiência pessoal será escrita mais perto do final do artigo), no entanto, por mais estranho que pareça, não havia um único artigo no hub que sequer mencionasse esse idioma de alguma forma e decidi que seria bom escrever sobre ele. Obviamente, o idioma possui documentação e um site oficial , mas nem todos lerão as observações, e o formato do artigo está mais próximo de muitos (inclusive eu).

Primeiro, falaremos sobre a sintaxe da linguagem e todos os seus recursos, depois sobre como implementá-la em seu projeto C ++ e, no final, falarei um pouco sobre minha experiência. Se alguma parte de você não estiver interessada ou desejar ler o artigo em uma ordem diferente, poderá usar o índice:



Sintaxe de idioma


O ChaiScript é muito semelhante ao C ++ e JS em sua sintaxe. Primeiro, ele, como a grande maioria das linguagens de script, é digitado dinamicamente, no entanto, ao contrário do JavaScript, possui digitação estrita (não 1 + "2" ). Há também um coletor de lixo embutido, a linguagem é totalmente interpretável, permitindo a execução de código linha por linha, sem compilação no bytecode. Ele suporta exceções (além disso, joint, permitindo capturá-las tanto dentro do script quanto em C ++), funções lambda, sobrecarga do operador. Não é sensível a espaços, permitindo que você escreva como uma única linha através de um ponto-e-vírgula ou no estilo python, separando expressões com uma nova linha.

Tipos primitivos


O ChaiScript, por padrão, armazena variáveis ​​inteiras como int, real como double e strings com std :: string. Isso é feito principalmente para garantir a compatibilidade com o código de chamada. A linguagem ainda possui sufixos para números, para que possamos indicar explicitamente que tipo de variável é nossa:

 /*   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 

Alterar o tipo de variáveis ​​simplesmente não funciona, provavelmente você precisará definir seu próprio operador `=` para esses tipos; caso contrário, você corre o risco de lançar uma exceção (falaremos sobre isso mais adiante) ou tornar-se vítima de arredondamentos, assim:

 var integer = 3 integer = 5.433 print(integer) //  5    double    int! integer = true //   -   `=`  (int, bool) 

No entanto, você pode declarar uma variável sem atribuir um valor a ela; nesse caso, ela conterá um tipo de indefinido até que seja atribuído um valor.

Recipientes em linha


O idioma possui dois contêineres - Vetor e Mapa. Eles funcionam de maneira muito semelhante aos seus equivalentes em C ++ (std :: vector e std :: map, respectivamente), mas não precisam de um tipo, porque podem armazenar qualquer um. A indexação pode ser feita como de costume com ints, mas o Map exige uma chave com uma string. Aparentemente inspirados em python, os autores também adicionaram a capacidade de declarar rapidamente contêineres no código usando a seguinte sintaxe:

 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 //       

Ambas as classes repetem quase completamente suas contrapartes em C ++, com exceção dos iteradores, porque em vez delas existem classes especiais Range e Const_Range. A propósito, todos os contêineres são passados ​​por referência, mesmo se você usar a atribuição por =, o que é muito estranho para mim, porque para todos os outros tipos ocorre a cópia por valor.

Construções Condicionais


Quase todas as construções de condições e ciclos podem ser descritas literalmente em um exemplo de código:

 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 } 

Acho que as pessoas familiarizadas com C ++ não encontraram nada de novo. Isso não é surpreendente, porque o ChaiScript está posicionado como uma linguagem fácil para os "alunos" aprenderem e, portanto, empresta todos os projetos clássicos conhecidos. Os autores decidiram destacar até duas palavras-chave para declarar variáveis ​​- var e auto , caso você realmente goste das vantagens com auto.

Contexto de execução


O ChaiScript possui um contexto local e global. O código é executado de cima para baixo, linha por linha, no entanto, pode ser retirado em funções e chamado mais tarde (mas não antes!). Variáveis ​​declaradas dentro de funções ou condições / loops não são visíveis por fora, por padrão, mas você pode alterar esse comportamento usando o identificador global vez de var . As variáveis ​​globais diferem das comuns, pois, primeiro, são visíveis fora do contexto local e, segundo, podem ser declaradas novamente (se o valor não for definido durante a declaração repetida, ele permanecerá o mesmo)

 //     chaiscript def foo(x) { global G = 2 print(x) } foo(0) //  foo(x), G = 2 print(G) //  2 global G = 3 //  G = 3,   global -  ! 

A propósito, se você possui uma variável e precisa verificar se está atribuído um valor, use a função is_var_undef , que retorna true se a variável não estiver definida.

Interpolação de string


Objetos base ou objetos de usuário que possuem um método to_string() podem ser colocados em uma string usando a sintaxe ${object} . Isso evita concatenações desnecessárias de strings e geralmente parece muito mais organizado:

 var x = 3 var y = 4 //  sum of 3 + 4 = 7 print("sum of ${x} + ${y} = ${x + y}") 

Vector, Map, MapPair e todas as primitivas também suportam esse recurso. O vetor é exibido no formato [o1, o2, ...] , Mapa como [<key1, val1>, <key2, val2>, ...] e MapPair: <key, val> .

Funções e suas nuances


As funções ChaiScript são objetos como todo o resto. Eles podem ser capturados, atribuídos a variáveis, aninhados em outras funções e transmitidos como argumento. Também para eles, você pode especificar o tipo de valores de entrada (que é o que faltavam nos idiomas digitados dinamicamente!). Para isso, é necessário especificar o tipo antes de declarar o parâmetro da função. Se, quando chamado, o parâmetro puder ser convertido para o especificado, a conversão ocorrerá de acordo com as regras do C ++, caso contrário, uma exceção será lançada:

 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) 

As funções no idioma também podem ser definidas como condições de chamada (proteção de chamada). Se não for respeitado, uma exceção será lançada; caso contrário, será feita uma chamada. Também observo que, se a função não tiver uma declaração de retorno no final, a última expressão será retornada. Muito conveniente para pequenas rotinas:

 def div(x, y) : y != 0 { x / y } //  `y`    -    `x`  `y` print(div(2, 0.5)) //  4.0 print(div(2, 0)) // , `y`  0! 

Classes e Dynamic_Object


O ChaiScript possui os rudimentos do OOP, que é uma vantagem definitiva se você precisar manipular objetos complexos. O idioma tem um tipo especial - Dynamic_Object. De fato, todas as instâncias de classes e espaços para nome são exatamente Dynamic_Object com propriedades predefinidas. Um objeto dinâmico permite adicionar campos a ele durante a execução do script e acessá-los:

 var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); } //  obj   f (     `x` obj.f(-3); //  0 

As classes são definidas de maneira bastante simples. Eles podem ser configurados para campos, métodos, construtores. Do set_explicit(object, value) interessante set_explicit(object, value) através da função especial set_explicit(object, value) você pode "consertar" os campos do objeto proibindo a adição de novos métodos ou atributos após a declaração da classe (isso geralmente é feito no construtor):

 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) //    wx = 3 //      Widget   x 

Um ponto importante - de fato, os métodos de classe são apenas funções cujo primeiro argumento é um objeto de uma classe com um tipo especificado explicitamente. Portanto, o código a seguir é equivalente a adicionar um método a uma classe existente:

 def set_id(Widget w, id) { w.id = id } w.set_id(9) // w.id = 9 set_id(w, 9) //  , w.id = 9 

Qualquer pessoa familiarizada com o C # pode substituir o que parece dolorosamente um método de extensão e estará próxima da verdade. Portanto, no idioma, você pode adicionar novas funcionalidades, mesmo para classes internas, por exemplo, para uma string ou int. Os autores também oferecem uma maneira complicada de sobrecarregar os operadores: para fazer isso, você precisa colocar o símbolo do operador com um til (`) como no exemplo abaixo:

 //   +     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 

Namespaces


Falando sobre o espaço para nome no ChaiScript, deve-se ter em mente que essas são essencialmente classes que sempre estão em um contexto global. Você pode criá-los usando a função de namespace(name) e, em seguida, adicionar as funções e classes necessárias. Por padrão, não há bibliotecas no idioma, no entanto, você pode instalá-las usando extensões, sobre as quais falaremos um pouco mais tarde. Em geral, a inicialização do espaço para nome pode ser assim:

 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 

Expressões Lambda e outros recursos


As expressões lambda no ChaiScript são semelhantes ao que sabemos em C ++. A palavra-chave divertida é usada para eles, e eles também exigem a especificação explícita das variáveis ​​capturadas, mas sempre fazem isso por referência. O idioma também possui uma função de ligação que permite vincular valores a parâmetros de função:

 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 

Exceções


Exceções podem ocorrer durante a execução do script. Eles podem ser interceptados tanto no próprio ChaiScript (que discutiremos aqui) quanto no C ++. A sintaxe é absolutamente idêntica às vantagens, você pode até jogar fora um número ou uma string:

 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 } 

De uma maneira boa, você deve definir sua classe de exceções e lançá-la. Falaremos sobre como interceptá-lo em C ++ na segunda seção. Para exceções de intérpretes, o ChaiScript lança suas exceções, como eval_error, bad_boxed_cast, etc.

Constantes intérpretes


Para minha surpresa, a linguagem acabou sendo algum tipo de macro de compilador - existem apenas quatro e todas servem para identificar o contexto e são usadas principalmente para tratamento de erros:
__LINE__linha atual, se o código não for executado a partir de um arquivo, então '1'
__FILE__arquivo atual, se o código não for chamado de um arquivo, "__EVAL__"
__CLASS__classe atual ou "NOT_IN_CLASS"
__FUNC__função atual ou "NOT_IN_FUNCTION"

Interceptação de erro


Se a função que você está chamando não tiver sido declarada, uma exceção será lançada. Se isso é inaceitável para você, você pode definir uma função especial - method_missing(object, func_name, params) , que será chamada com os argumentos correspondentes em caso de erro:

 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 

Funções incorporadas


O ChaiScript define muitas funções internas e, no artigo, gostaria de falar sobre funções especialmente úteis. Entre eles: 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>] 


Implementação em C ++


Instalação


O ChaiScript é uma biblioteca somente de cabeçalho baseada em modelo C ++. Portanto, para a instalação, você só precisa criar um repositório clone ou apenas colocar todos os arquivos desta pasta em seu projeto. Como, dependendo do IDE, tudo isso é feito de maneira diferente e foi descrito em detalhes nos fóruns por um longo tempo, assumiremos que você conseguiu conectar a biblioteca e o código com include: #include <chaiscript/chaiscript.hpp> compilado.

Chamada de código C ++ e carregamento de script


O menor código de amostra usando o ChaiScript é mostrado abaixo. Definimos uma função simples em C ++ que pega std :: string e retorna a string alterada e, em seguida, adicionamos um link a ela no objeto ChaiScript para chamá-la. A compilação pode levar um tempo considerável, mas isso se deve principalmente ao fato de não ser fácil instanciar um grande número de modelos para o compilador:

 #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")); )"); } 

Espero que você tenha tido sucesso e tenha visto o resultado da função. Quero anotar uma nuance imediatamente - se você declarar um objeto ChaiScript como estático, receberá um erro de execução desagradável. Isso ocorre porque o idioma suporta multithreading por padrão e armazena variáveis ​​de fluxo local que são acessadas em seu destruidor. No entanto, eles são destruídos antes que o destruidor da instância estática seja chamado e, como resultado, temos uma violação de acesso ou erro de falha de segmentação. Com base no problema do github , a solução mais simples seria simplesmente colocar #define CHAISCRIPT_NO_THREADS nas configurações do compilador ou antes de incluir o arquivo da biblioteca, desativando o multithreading. Pelo que entendi, não foi possível corrigir esse erro.

Agora vamos analisar em detalhes como ocorre a interação entre C ++ e ChaiScript. A biblioteca define uma função de modelo especial fun , que pode levar um ponteiro para uma função, functor ou ponteiro para uma variável de classe e, em seguida, retornar um objeto especial que armazena estado. Como exemplo, vamos definir a classe Widget no código C ++ e tentar associá-la ao ChaiScript de diferentes maneiras:

 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 )"); } 

Como você pode ver, o ChaiScript funciona de maneira absolutamente calma com classes C ++ desconhecidas e pode chamar seus métodos. Se você cometer algum erro em algum lugar do código, provavelmente o script lançará uma exceção do tipo de error in function dispatch , o que não é crítico. No entanto, não apenas as funções podem ser importadas, vamos ver como adicionar uma variável a um script usando a biblioteca. Para fazer isso, selecione um pouco mais a tarefa - importe std :: vector <Widget>. A função chaiscript::var e o método add_global nos ajudarão com isso. Também adicionaremos o campo público Data ao nosso Widget para ver como importar o campo da classe:

 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]) } )"); } 

O código acima exibe: widget #1 with data: 0 , widget #2 with data: 2 , widget #3 with data: 4 . Adicionamos um ponteiro ao campo de classe no ChaiScript e, como o campo acabou sendo um tipo primitivo, alteramos seu valor. Além disso, vários métodos foram adicionados para trabalhar com std::vector , incluindo o operator[] . Quem conhece o STL sabe que o std::vector dois métodos de indexação - um retorna um link constante e o outro, um link simples. É por isso que, para funções sobrecarregadas, você deve indicar explicitamente o tipo delas - caso contrário, a ambiguidade surge e o compilador gerará um erro.

A biblioteca fornece vários outros métodos para adicionar objetos, mas todos são quase idênticos; portanto, não vejo o ponto de considerá-los em detalhes. Como uma pequena dica, aqui está o código abaixo:

 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 

Usando contêineres STL


Se desejar passar contêineres STL contendo tipos primitivos para o ChaiScript, você poderá adicionar uma instanciação de contêiner de modelo ao seu script para não importar métodos para cada tipo.

 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) } )"); 

Sob o capô, várias funções do ChaiScript são chamadas, as quais adicionam os métodos necessários. Em geral, se sua classe suportar operações semelhantes com contêineres STL, você também poderá adicioná-lo dessa maneira. std::vector<Widget>Infelizmente, no caso de c, isso é impossível, pois o ChaiScript requer um construtor sem parâmetros para o elemento vector_type, que nosso Widget não possuía.

Classes C ++ dentro do ChaiScript


Talvez como parte de sua tarefa, você precise não apenas modificar objetos no ChaiScript, mas também criá-los em um script. Bem, isso é inteiramente possível. Vamos pegar a classe Widget novamente, por exemplo, e herdar a classe WindowWidget, e adicionar ao script a capacidade de criar as duas e também converter a classe herdada na base:

 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}") )"); } 

O polimorfismo funciona no ChaiScript exatamente da mesma maneira que no C ++ para os tipos sobre os quais você fornece informações. Se, por algum motivo, houver ambiguidade ao adicionar um ponteiro a um método herdado (talvez a classe seja herdada de vários métodos básicos de uma só vez), leve-a explicitamente à classe desejada, como foi feito no exemplo acima com o operador de indexação std::vector<Widget>.

Vinculando uma instância a um método e convertendo um tipo


Para objetos singleton, é conveniente usar a captura de links para eles junto com um método ou campo. Nesse caso, no ChaiScript, obtemos uma função ou uma variável global que pode ser acessada sem mencionar este objeto:

 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) )"); 

Além disso, ao exportar mais classes de "biblioteca" de C ++ para ChaiScript (por exemplo, vec3, complexo, matriz), a possibilidade de conversão implícita de um tipo para outro geralmente é necessária. No ChaiScript, esse problema é resolvido adicionando type_conversionum script ao objeto. Por exemplo, considere a classe Complex e a implementação da conversão de int e double para ela durante a adição:

 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` } 

Portanto, não é necessário escrever uma função de conversão no próprio C ++ e somente exportá-la para o ChaiScript. Você pode adicionar transformações e já descrever a nova funcionalidade no próprio código de script. Se a conversão para os dois tipos não for trivial, você pode passar o lambda como argumento para uma função type_conversion. Será chamado quando estiver lançando.

Um princípio semelhante é usado para converter Vector ou Map ChaiScript em seu tipo personalizado. Para isso, vector_conversione são definidos na biblioteca map_conversion.

Descompactando valores de retorno ChaiScript


Métodos evale eval_fileretorne o valor da última expressão executada como um objeto Boxed_Value. Para descompactá-lo e usar o resultado no código C ++, você pode especificar explicitamente o tipo do valor de retorno ou usar uma função boxed_cast<T>. Se a conversão entre tipos existir, ela será executada; caso contrário, será gerada uma exceção 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); 

Como todos os objetos dentro do ChaiScript são armazenados usando shared_ptr, você pode obter o objeto como um ponteiro para continuar trabalhando com ele. Para fazer isso, especifique explicitamente o tipo shared_ptr ao converter o valor de retorno:

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

O principal é não manter uma referência ao valor do shared_ptr desreferenciado, caso contrário, você corre o risco de obter uma violação de acesso depois que a variável é excluída durante a coleta de lixo automática no script.

Como variáveis, você pode obter funções do ChaiScript na forma de functores compactados que capturam o estado de um objeto ChaiScript. Por exemplo, usaremos a funcionalidade já implementada da classe Complex e tentaremos usá-la para chamar uma função no estágio de execução do programa:

 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` 

Captura de exceção do ChaiScript


Os autores recomendam a captura de três tipos de exceções além daquelas que você mesmo gera. Isso ocorre eval_errorpara erros de tempo de execução, bad_boxed_castchamados quando os valores de retorno são descompactados incorretamente e std::exceptionpara todo o resto. Se você planeja lançar suas próprias exceções, pode configurar a conversão automática para tipos C ++:

 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(); } } 

O exemplo acima mostra como capturar a maioria das exceções em C ++. Além do método pretty_print, eval_errorainda existem muitos dados úteis, como a pilha de chamadas, o nome do arquivo, os detalhes do erro, mas não entraremos tanto nessa classe neste artigo.

Bibliotecas ChaiScript


Infelizmente, por padrão, o ChaiScript não fornece funcionalidade adicional em termos de bibliotecas. Por exemplo, falta funções matemáticas, tabelas de hash e a maioria dos algoritmos. Você pode fazer o download de alguns deles na forma de bibliotecas de módulos do repositório oficial do ChaiScript Extras e depois importar para o seu script. Por exemplo, considere a biblioteca de matemática e a função 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 } 

Você também pode escrever sua biblioteca para o idioma e depois importar. Isso é feito de maneira simples, por isso aconselho que você se familiarize com a matemática de código aberto ou qualquer outra fonte no repositório. Em princípio, como parte da integração com o C ++, examinamos quase tudo, então acho que a seção pode ser concluída sobre isso.

Experiência pessoal


No momento, estou escrevendo um mecanismo 3D no OpenGL como um projeto pessoal e tive uma ideia completamente lógica de implementar um console de depuração para controlar o estado do aplicativo em tempo real por meio de comandos. Seria possível fazer ciclismo , é claro , mas como se costuma dizer, “o jogo não valeria a pena”, então decidi levar a biblioteca pronta.

Como mencionei no início do artigo, eu já sabia sobre o ChaiScript, então tive uma escolha entre ele e Lua. Até aquele momento, eu não estava familiarizado com nenhum desses idiomas, portanto, fatores como: sintaxe clara, facilidade de incorporação no código existente e suporte ao C ++ em vez de C influenciaram mais para não fazer diferença entre os invólucros de OOP sobre C- funções de estilo. Penso que, ao ler este artigo, você já adivinhou em que minha escolha recaiu.

No momento, o idioma é mais do que adequado para mim, e escrever sobre as aulas não é grande coisa. No código do mecanismo, uma instância do console no ImGui é anexada ao aplicativo iniciado, no qual o objeto chaiscript é inicializado. Com algumas macros, a tarefa de introduzir uma nova classe em um script se resume a uma descrição simples de todos os métodos que precisam ser exportados:

 //      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); 

Da mesma maneira, várias outras classes são exportadas e, em seguida, tudo é conectado por funções lambda declaradas diretamente no código de inicialização. Você pode ver o resultado do script na captura de tela: o

imagem
console do chaiscript para o ImGui: baixando e instalando o objeto por meio de comandos

Dada a flexibilidade geral da biblioteca, a mudança na abordagem de exportação de classes para o script será quase direta. É claro que Lua possui uma documentação mais extensa e uma comunidade, e essa linguagem seria preferível se você precisar obter mais desempenho do código de script (o JIT ainda faz seu trabalho), mas você ainda não deve cancelar o ChaiScript. Se você tem um projeto pequeno que precisa de script, pode experimentar com segurança as alternativas disponíveis.

Nesta nota, eu gostaria de concluir este artigo. Se você já teve experiência em trabalhar com linguagens de script dentro do C ++ (seja Lua ou outra linguagem), nos comentários, ficarei feliz em ouvir sua opinião sobre o ChaiScript e os scripts em geral. Também saúdo quaisquer perguntas ou comentários sobre a publicação. Obrigado a todos pela leitura.

Links úteis


Source: https://habr.com/ru/post/pt483540/


All Articles