Cuando existe la necesidad de incrustar un lenguaje de script en un proyecto C ++, lo primero que la mayoría de la gente recuerda es Lua. En este artículo no será, hablaré de otro lenguaje no menos conveniente y fácil de aprender llamado ChaiScript.

Breve introducción
Yo mismo me topé con ChaiScript por accidente cuando vi
una de las conferencias de Jason Turner, uno de los creadores del lenguaje. Me interesó, y en ese momento, cuando era necesario elegir un lenguaje de script en el proyecto, decidí: ¿por qué no probar ChaiScript? El resultado me sorprendió gratamente (mi experiencia personal se escribirá más cerca del final del artículo), sin embargo, no importa cuán extraño pueda sonar, no había un solo artículo en el centro que mencionara este idioma al menos de alguna manera, y decidí que Sería bueno escribir sobre él. Por supuesto, el lenguaje tiene
documentación y un
sitio oficial , pero no todos lo leerán de las observaciones, y el formato del artículo está más cerca de muchos (incluido yo).
Primero, hablaremos sobre la sintaxis del lenguaje y todas sus características, luego sobre cómo implementarlo en su proyecto C ++, y al final hablaré un poco sobre mi experiencia. Si alguna parte de usted no está interesada, o si desea leer el artículo en un orden diferente, puede usar la tabla de contenido:
Sintaxis del lenguaje
ChaiScript es muy similar a C ++ y JS en su sintaxis. En primer lugar, al igual que la gran mayoría de los lenguajes de secuencias de comandos, se escribe dinámicamente, sin embargo, a diferencia de JavaScript, tiene una escritura estricta (no
1 + "2"
). También hay un recolector de basura incorporado, el lenguaje es totalmente interpretable, lo que le permite ejecutar código línea por línea, sin compilar en bytecode. Tiene soporte para excepciones (además, conjunto, lo que le permite atraparlos tanto dentro del script como en C ++), funciones lambda, sobrecarga del operador. No es sensible a los espacios, lo que le permite escribir como una sola línea a través de un punto y coma, o en estilo python, separando las expresiones con una nueva línea.
Tipos primitivos
ChaiScript almacena de forma predeterminada las variables enteras como int, real como double y cadenas con std :: string. Esto se hace principalmente para garantizar la compatibilidad con el código de llamada. El lenguaje incluso tiene sufijos para los números, por lo que podemos indicar explícitamente de qué tipo es nuestra variable:
var myInt = 1
Cambiar el tipo de variables simplemente no funciona, lo más probable es que necesite definir su propio operador `=` para estos tipos, de lo contrario corre el riesgo de lanzar una excepción (hablaremos de esto más adelante) o convertirse en una víctima de redondeo, así:
var integer = 3 integer = 5.433 print(integer)
Sin embargo, puede declarar una variable sin asignarle un valor, en cuyo caso contendrá una especie de indefinido hasta que se le asigne un valor.
Contenedores en línea
El lenguaje tiene dos contenedores: Vector y Mapa. Funcionan de manera muy similar a sus contrapartes en C ++ (std :: vector y std :: map, respectivamente), pero no requieren un tipo, ya que pueden almacenar cualquiera. La indexación se puede hacer como de costumbre con ints, pero Map requiere una clave con una cadena. Aparentemente inspirados en python, los autores también agregaron la capacidad de declarar rápidamente contenedores en código usando la siguiente sintaxis:
var v = [ 1, 2, 3u, 4ll, "16", `+` ]
Ambas clases repiten casi por completo sus contrapartes en C ++, con la excepción de los iteradores, porque en su lugar hay clases especiales Range y Const_Range. Por cierto, todos los contenedores se pasan por referencia, incluso si usa la asignación a través de =, lo cual es muy extraño para mí, porque para todos los demás tipos se produce la copia por valor.
Construcciones condicionales
Casi todas las construcciones de condiciones y ciclos se pueden describir literalmente en un código de ejemplo:
var a = 5 var b = -1
Creo que las personas familiarizadas con C ++ no han encontrado nada nuevo. Esto no es sorprendente, porque ChaiScript se posiciona como un lenguaje fácil de aprender para los "estudiantes" y, por lo tanto, toma prestados todos los diseños clásicos conocidos. Los autores decidieron resaltar incluso dos palabras clave para declarar variables:
var
y
auto
, en caso de que realmente te gusten las ventajas con auto.
Contexto de ejecución
ChaiScript tiene un contexto local y global. El código se ejecuta de arriba a abajo línea por línea, sin embargo, se puede extraer en funciones y llamar más tarde (¡pero no antes!). Las variables declaradas dentro de las funciones o condiciones / bucles no son visibles por defecto desde el exterior, pero puede cambiar este comportamiento utilizando el identificador
global
lugar de
var
. Las variables globales difieren de las ordinarias en que, en primer lugar, son visibles fuera del contexto local y, en segundo lugar, se pueden volver a declarar (si el valor no se establece durante la declaración repetida, entonces permanece igual)
Por cierto, si tiene una variable y necesita verificar si se le asigna un valor, use la
is_var_undef
incorporada
is_var_undef
, que devuelve verdadero si la variable no está definida.
Interpolación de cuerdas
Los objetos base u objetos de usuario que tienen un método
to_string()
se pueden poner en una cadena usando la sintaxis
${object}
. Esto evita concatenaciones de cadenas innecesarias y generalmente se ve mucho más ordenado:
var x = 3 var y = 4
Vector, Map, MapPair y todas las primitivas también admiten esta función. El vector se muestra en el formato
[o1, o2, ...]
, Mapa como
[<key1, val1>, <key2, val2>, ...]
y MapPair:
<key, val>
.
Funciones y sus matices.
Las funciones ChaiScript son objetos como todo lo demás. Se pueden capturar, asignar a variables, anidar en otras funciones y pasar como argumento. También para ellos puede especificar el tipo de valores de entrada (que es lo que les faltaba a los idiomas escritos dinámicamente). Para esto, debe especificar el tipo antes de declarar el parámetro de función. Si, cuando se llama, el parámetro se puede convertir al especificado, la conversión se realizará de acuerdo con las reglas de C ++; de lo contrario, se generará una excepción:
def adder(int x, int y) { return x + y } def adder(bool x, bool y) { return x || y } adder(1, 2)
Las funciones en el idioma también se pueden establecer condiciones de llamada (guardia de llamada). Si no se respeta, se lanza una excepción; de lo contrario, se realiza una llamada. También noto que si la función no tiene una declaración de retorno al final, entonces se devolverá la última expresión. Muy conveniente para pequeñas rutinas:
def div(x, y) : y != 0 { x / y }
Clases y Dynamic_Object
ChaiScript tiene los rudimentos de OOP, que es una ventaja definitiva si necesita manipular objetos complejos. El idioma tiene un tipo especial: Dynamic_Object. De hecho, todas las instancias de clases y espacios de nombres son exactamente Dynamic_Object con propiedades predefinidas. Un objeto dinámico le permite agregarle campos durante la ejecución del script y luego acceder a ellos:
var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); }
Las clases se definen de manera bastante simple. Se pueden establecer en campos, métodos, constructores. Desde el
set_explicit(object, value)
interesante
set_explicit(object, value)
través de la función especial
set_explicit(object, value)
puede "arreglar" los campos del objeto al prohibir la adición de nuevos métodos o atributos después de la declaración de clase (esto generalmente se hace en el constructor):
class Widget { var id;
Un punto importante: de hecho, los métodos de clase son solo funciones cuyo primer argumento es un objeto de una clase con un tipo explícitamente especificado. Por lo tanto, el siguiente código es equivalente a agregar un método a una clase existente:
def set_id(Widget w, id) { w.id = id } w.set_id(9)
Cualquier persona familiarizada con C # puede reemplazar lo que dolorosamente parece un método de extensión, y estará cerca de la verdad. Por lo tanto, en el lenguaje puede agregar una nueva funcionalidad incluso para las clases integradas, por ejemplo, para una cadena o int. Los autores también ofrecen una forma complicada de sobrecargar a los operadores: para hacer esto, debe rodear el símbolo del operador con una tilde (`) como en el siguiente ejemplo:
Espacios de nombres
Hablando sobre el espacio de nombres en ChaiScript, debe tenerse en cuenta que estas son esencialmente clases que siempre están en un contexto global. Puede crearlos utilizando la función de
namespace(name)
y luego agregar las funciones y clases necesarias. De manera predeterminada, no hay bibliotecas en el idioma, sin embargo, puede instalarlas usando extensiones, de las que hablaremos más adelante. En general, la inicialización del espacio de nombres podría verse así:
namespace("math")
Expresiones lambda y otras características
Las expresiones lambda en ChaiScript son similares a lo que sabemos de C ++. La palabra clave
divertida se usa para ellos, y también requieren que se especifiquen explícitamente las variables capturadas, sin embargo, siempre lo hacen por referencia. El lenguaje también tiene una función de vinculación que le permite vincular valores a parámetros de función:
var func_object = fun(x) { x * x } func_object(9)
Excepciones
Pueden ocurrir excepciones durante la ejecución del script. Se pueden interceptar tanto en ChaiScript (que discutiremos aquí) como en C ++. La sintaxis es absolutamente idéntica a las ventajas, incluso puede arrojar un número o una cadena:
try { eval(x + 1)
En el buen sentido, debe definir su clase de excepciones y lanzarla. Hablaremos sobre cómo interceptarlo en C ++ en la segunda sección. Para las excepciones de intérpretes, ChaiScript arroja sus excepciones, como eval_error, bad_boxed_cast, etc.
Constantes de intérprete
Para mi sorpresa, el lenguaje resultó ser algún tipo de macros de compilación: solo hay 4 de ellas y todas sirven para identificar el contexto y se utilizan principalmente para el manejo de errores:
Error de captura
Si la función que está llamando no se ha declarado, se produce una excepción. Si esto es inaceptable para usted, puede definir una función especial:
method_missing(object, func_name, params)
, que se llamará con los argumentos correspondientes en caso de error:
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)
Funciones incorporadas
ChaiScript define muchas funciones integradas, y en el artículo me gustaría hablar sobre funciones especialmente útiles. Entre ellos:
eval(str)
,
eval_file(filename)
,
to_json(object)
,
from_json(str)
:
var x = 3 var y = 5 var res = eval("x * y")
Implementación en C ++
Instalación
ChaiScript es una biblioteca de solo encabezado basada en plantillas C ++. En consecuencia, para la instalación solo necesita hacer un
repositorio de clones o simplemente poner todos los archivos de
esta carpeta en su proyecto. Dado que, dependiendo del IDE, todo esto se hace de manera diferente y se ha descrito en detalle en los foros durante mucho tiempo, entonces asumiremos que logró conectar la biblioteca, y el código con incluye:
#include <chaiscript/chaiscript.hpp>
compilado.
Invocación de código C ++ y carga de script
El código de muestra más pequeño que usa ChaiScript es el que se muestra a continuación. Definimos una función simple en C ++ que toma std :: string y devuelve la cadena modificada, y luego le agregamos un enlace en el objeto ChaiScript para llamarla. La compilación puede llevar un tiempo considerable, pero esto se debe principalmente al hecho de que crear instancias de una gran cantidad de plantillas para el compilador no es fácil:
#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 haya tenido éxito y haya visto el resultado de la función. Quiero notar un matiz de inmediato: si declara un objeto ChaiScript como estático, obtendrá un error de tiempo de ejecución desagradable. Esto se debe al hecho de que el lenguaje admite subprocesos múltiples de forma predeterminada y almacena variables de flujo locales a las que se accede en su destructor. Sin embargo, se destruyen antes de que se llame al destructor de la instancia estática y, como resultado, tenemos una infracción de acceso o un error de segmentación. Según el
problema en github , la solución más simple sería simplemente poner
#define CHAISCRIPT_NO_THREADS
en la configuración del compilador o antes de incluir el archivo de la biblioteca, deshabilitando así el subprocesamiento múltiple. Según tengo entendido, no fue posible corregir este error.
Ahora analizaremos en detalle cómo se produce la interacción entre C ++ y ChaiScript. La biblioteca define una función de plantilla especial
fun
, que puede llevar un puntero a una función, functor o puntero a una variable de clase, y luego devolver un objeto especial que almacena el estado. Como ejemplo, definamos la clase Widget en código C ++ e intentemos asociarla con ChaiScript de diferentes maneras:
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 puede ver, ChaiScript funciona con absoluta calma con clases de C ++ desconocidas y puede llamar a sus métodos. Si comete un error en algún lugar del código, lo más probable es que el script arroje una excepción del
error in function dispatch
amable
error in function dispatch
, que no es crítico en absoluto. Sin embargo, no solo se pueden importar funciones, veamos cómo agregar una variable a un script usando la biblioteca. Para hacer esto, seleccione la tarea un poco más difícil: importe std :: vector <Widget>. La función
chaiscript::var
y el método
add_global
nos ayudarán con esto. También agregaremos el campo público
Data
a nuestro widget para ver cómo importar el campo de clase:
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;
El código anterior muestra:
widget #1 with data: 0
, widget #2 with data: 2
, widget #3 with data: 4
. Agregamos un puntero al campo de clase en ChaiScript, y dado que el campo resultó ser un tipo primitivo, cambiamos su valor. Además, se agregaron varios métodos para trabajar con
std::vector
, incluido el
operator[]
. Quienes estén familiarizados con STL saben que
std::vector
dos métodos de indexación: uno devuelve un enlace constante y el otro un enlace simple. Es por eso que para las funciones sobrecargadas, debe indicar explícitamente su tipo; de lo contrario, surge la ambigüedad y el compilador arrojará un error.
La biblioteca proporciona varios métodos más para agregar objetos, pero todos son casi idénticos, por lo que no veo el punto de considerarlos en detalle. Como una pequeña pista, aquí está el siguiente código:
chai.add(chaiscript::var(x), "x");
Usando contenedores STL
Si desea pasar contenedores STL que contienen tipos
primitivos a ChaiScript, puede agregar una instancia de contenedor de plantillas a su script para que no importe 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");
Bajo el capó, se llaman varias funciones ChaiScript, que a su vez agregan los métodos necesarios. En general, si su clase admite operaciones similares con contenedores STL, también puede agregarlo de esta manera. En el caso de c, std::vector<Widget>
esto, desafortunadamente, es imposible, ya que ChaiScript requiere un constructor sin parámetros para el elemento vector_type
, que nuestro Widget no tenía.Clases de C ++ dentro de ChaiScript
Quizás como parte de su tarea, necesita no solo modificar objetos en ChaiScript, sino también crearlos en un script. Bueno, esto es completamente posible. Tomemos la clase Widget nuevamente, por ejemplo, y heredemos la clase WindowWidget de ella, y luego agreguemos al script la capacidad de crear ambos y también convertir la clase heredada a la 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;
El polimorfismo funciona en ChaiScript exactamente de la misma manera que en C ++ para los tipos sobre los que proporciona información. Si por alguna razón hay ambigüedad al agregar un puntero a un método heredado (tal vez la clase se hereda de varios métodos básicos a la vez), tráigala a la clase deseada explícitamente, como se hizo en el ejemplo anterior con el operador de indexación std::vector<Widget>
.Vinculando una instancia a un método y convirtiendo un tipo
Para objetos singleton, es conveniente usar la captura de enlaces a ellos junto con un método o campo. En este caso, en ChaiScript obtenemos una función o una variable global a la que se puede acceder sin mencionar este objeto: Widget w(3); w.Data = 4444;
Además, al exportar más clases de "biblioteca" de C ++ a ChaiScript (por ejemplo, vec3, complejo, matriz), a menudo se requiere la posibilidad de conversión implícita de un tipo a otro. En ChaiScript, este problema se resuelve agregando type_conversion
un script al objeto. Por ejemplo, considere la clase compleja y la implementación de convertir int y double a ella durante la suma: class Complex { public: float Re, Im; Complex(float re, float im = 0.0f) : Re(re), Im(im) { } }; int main() { chaiscript::ChaiScript chai;
Por lo tanto, no es necesario escribir una función de conversión en C ++, y solo luego exportarla a ChaiScript. Puede agregar transformaciones y ya describir la nueva funcionalidad en el código del script. Si la conversión para los dos tipos no es trivial, puede pasar el lambda como argumento a una función type_conversion
. Se llamará cuando se lance.Se utiliza un principio similar para convertir ChaiScript Vector o Map en su tipo personalizado. Para esto, vector_conversion
y se definen en la biblioteca map_conversion
.Desempaquetando los valores de retorno de ChaiScript
Métodos eval
y eval_file
devolver el valor de la última expresión ejecutada como un objeto Boxed_Value
. Para descomprimirlo y usar el resultado en código C ++, puede especificar explícitamente el tipo del valor de retorno o usar una función boxed_cast<T>
. Si existe la conversión entre tipos, se ejecutará, de lo contrario se generará una excepción bad_boxed_cast
:
Dado que todos los objetos dentro de ChaiScript se almacenan usando shared_ptr, puede obtener el objeto como un puntero para seguir trabajando con él. Para hacer esto, especifique explícitamente el tipo shared_ptr al convertir el valor de retorno: auto x = chai.eval<std::shared_ptr<double>>("var x = 3.2");
Lo principal es no mantener una referencia al valor de shared_ptr desreferenciado, de lo contrario corre el riesgo de obtener una infracción de acceso después de que la variable se elimine durante la recolección de basura automática en el script.Al igual que las variables, puede obtener funciones de ChaiScript en forma de functores empaquetados que capturan el estado de un objeto ChaiScript. Por ejemplo, usaremos la funcionalidad ya implementada de la clase Complex e intentaremos usarla para llamar a una función en la etapa de ejecución del programa: auto printComplex = chai.eval<std::function<void(Complex)>>(R"( fun(Complex c) { print("${c.re} + ${c.im}i"); } )");
Captura de excepciones ChaiScript
Los autores recomiendan capturar tres tipos de excepciones además de las que usted genera. Esto es eval_error
para errores de tiempo de ejecución, bad_boxed_cast
que se llama cuando los valores de retorno se desempaquetan incorrectamente y std::exception
para todo lo demás. Si planea lanzar sus propias excepciones, puede configurar la conversión automática a tipos C ++: class MyException : public std::exception { public: int Data; MyException(int data) : std::exception("MyException"), Data(data) { } }; int main() { chaiscript::ChaiScript chai;
El ejemplo anterior muestra cómo capturar la mayoría de las excepciones en C ++. Además del método pretty_print
, eval_error
todavía hay muchos datos útiles, como la pila de llamadas, el nombre del archivo, los detalles del error, pero no entraremos tanto en esta clase en este artículo.Bibliotecas ChaiScript
Desafortunadamente, por defecto, ChaiScript no proporciona funcionalidad adicional en términos de bibliotecas. Por ejemplo, carece de funciones matemáticas, tablas hash y la mayoría de los algoritmos. Puede descargar algunos de ellos en forma de bibliotecas de módulos desde el repositorio oficial de Extras de ChaiScript y luego importarlos a su script. Por ejemplo, tome la biblioteca matemática y la función 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 }
También puede escribir su biblioteca para el idioma y luego importar. Esto se hace de manera bastante simple, por lo que le aconsejo que se familiarice con las matemáticas de código abierto o cualquier otra fuente en el repositorio. En principio, como parte de la integración con C ++, examinamos casi todo, por lo que creo que la sección se puede completar al respecto.Experiencia personal
En este momento estoy escribiendo un motor 3D en OpenGL como un proyecto personal, y tuve una idea completamente lógica de implementar una consola de depuración para controlar el estado de la aplicación en tiempo real a través de comandos. Por supuesto, uno podría hacer ciclismo , pero como dicen, "el juego no valdría la pena", así que decidí tomar la biblioteca terminada.Como mencioné al principio del artículo, ya sabía sobre ChaiScript, así que tenía que elegir entre él y Lua. Hasta ese momento, no estaba familiarizado con ninguno de los dos idiomas, por lo tanto, factores tales como: sintaxis clara, facilidad de incrustación en el código existente y soporte para C ++ en lugar de C influyeron más para no hacer la valla de los contenedores OOP sobre C- funciones de estilo Creo que, mientras leía este artículo, ya adivinó en qué consistía mi elección.Por el momento, el idioma es más que adecuado para mí, y escribir sobre las clases no es gran cosa. En el código del motor, una instancia de la consola en ImGui se adjunta a la aplicación iniciada, en la que se inicializa el objeto chaiscript. Con un par de macros, la tarea de introducir una nueva clase en un script se reduce a una descripción simple de todos los métodos que deben exportarse:
Del mismo modo, se exportan varias clases más, y luego todo se conecta entre sí mediante funciones lambda declaradas directamente en el código de inicialización. Puede ver el resultado del script en la captura de pantalla: la
consola de chaiscript a ImGui: descarga e instalación del objeto a través de comandosDada la flexibilidad general de la biblioteca, cambiar el enfoque para exportar clases al script será casi sencillo. Por supuesto, Lua tiene una documentación más extensa y una comunidad, y este lenguaje sería preferible si necesita obtener más rendimiento del código de script (JIT todavía hace su trabajo), pero aún así no debe descartar ChaiScript. Si tiene un proyecto pequeño que necesita secuencias de comandos, puede experimentar con seguridad con las alternativas disponibles.En esta nota, me gustaría completar este artículo. Si ya tenía experiencia trabajando con lenguajes de secuencias de comandos dentro de C ++ (ya sea Lua u otro idioma), en los comentarios me complacerá escuchar su opinión sobre ChaiScript y las secuencias de comandos en general. También agradezco cualquier pregunta o comentario sobre la publicación. Gracias a todos por leer.Enlaces utiles