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.

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:
var myInt = 1
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)
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", `+` ]
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
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)
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
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)
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 }
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); }
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;
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)
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:
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")
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)
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)
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:
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)
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")
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);
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;
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");
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");
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;
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;
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_conversion
um 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;
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_conversion
e são definidos na biblioteca map_conversion
.Descompactando valores de retorno ChaiScript
Métodos eval
e eval_file
retorne 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
:
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"); } )");
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_error
para erros de tempo de execução, bad_boxed_cast
chamados quando os valores de retorno são descompactados incorretamente e std::exception
para 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;
O exemplo acima mostra como capturar a maioria das exceções em C ++. Além do método pretty_print
, eval_error
ainda 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:
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
console do chaiscript para o ImGui: baixando e instalando o objeto por meio de comandosDada 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