当需要在C ++项目中嵌入脚本语言时,大多数人记得的第一件事就是Lua。 在本文中,我不会再谈论另一种同样便捷且易学的语言,称为ChaiScript。

简短介绍
当我观看语言的创建者之一Jason Turner的其中一场
演讲时,我自己偶然发现了ChaiScript。 它引起了我的兴趣,当需要在项目中选择脚本语言时,我决定-为什么不尝试ChaiScript? 结果使我感到惊喜(我的个人经历将在本文结尾处写),但是,无论听起来多么奇怪,枢纽中都没有哪一篇文章以某种方式提到了这种语言,因此我决定写关于他的事会很高兴。 当然,该语言有
文档和一个
官方网站 ,但是并非所有人都会从观察中阅读该语言,并且文章的格式更接近于许多人(包括我)。
首先,我们将讨论该语言的语法及其所有功能,然后讨论如何在您的C ++项目中实现该语言,最后,我会谈一些我的经验。 如果您的某些部分不感兴趣,或者您想以不同的顺序阅读文章,则可以使用目录:
语言语法
ChaiScript的语法与C ++和JS非常相似。 首先,它像绝大多数脚本语言一样是动态类型的,但是,与JavaScript不同,它具有严格的类型(没有
1 + "2"
)。 还有一个内置的垃圾收集器,该语言是完全可解释的,允许您逐行执行代码,而无需编译为字节码。 它支持异常(此外,联合,允许您在脚本和C ++中捕获它们),lambda函数,运算符重载。 它对空格不敏感,允许您通过分号或python样式以单行形式编写,并用新行分隔表达式。
原始类型
默认情况下,ChaiScript将整数变量存储为int,实数作为double以及带有std :: string的字符串。 这样做主要是为了确保与调用代码的兼容性。 该语言甚至都有数字后缀,因此我们可以明确指出变量的类型:
var myInt = 1
更改变量的类型实际上是行不通的,很可能您需要为这些类型定义自己的`=`运算符,否则,您可能会抛出异常(稍后再讨论)或成为舍入的受害者,如下所示:
var integer = 3 integer = 5.433 print(integer)
但是,您可以在不给变量赋值的情况下声明变量,在这种情况下,变量将包含一种未定义的变量,直到为其分配了值。
内联容器
该语言有两个容器-矢量和地图。 它们的工作方式与C ++中的类似物非常相似(分别为std :: vector和std :: map),但是它们不需要类型,因为它们可以存储任何类型。 可以像往常一样使用int进行索引,但是Map需要带有字符串的键。 显然是受python启发的,作者还添加了使用以下语法在代码中快速声明容器的功能:
var v = [ 1, 2, 3u, 4ll, "16", `+` ]
除了迭代器外,这两个类几乎都完全重复了C ++中的对应类,因为除了它们之外,还有特殊的类Range和Const_Range。 顺便说一句,即使您通过=使用分配,所有容器也都通过引用传递,这对我来说很奇怪,因为对于所有其他类型,都会发生按值复制。
条件构造
条件和周期的几乎所有构造都可以用字面上的一个示例代码来描述:
var a = 5 var b = -1
我认为熟悉C ++的人们没有发现任何新东西。 这并不奇怪,因为ChaiScript被定位为“学习者”学习的一种简便语言,因此借鉴了所有著名的古典设计。 如果您真的很喜欢auto的优点,作者决定甚至突出显示两个用于声明变量的关键字
var
和auto。
执行上下文
ChaiScript具有本地和全局上下文。 该代码从上到下逐行执行,但是可以在函数中取出并稍后调用(但不能更早!)。 默认情况下,从外部看不到在函数或条件/循环内部声明的变量,但是您可以使用
global
标识符而不是
var
更改此行为。 全局变量与普通变量的不同之处在于,首先,它们在局部上下文之外可见,其次,可以重新声明(如果在重新声明期间未设置该值,则它保持不变)
顺便说一句,如果您有一个变量,并且需要检查是否为其分配了值,请使用
is_var_undef
内置函数,如果未定义该变量,则该函数返回true。
字符串插值
可以使用
${object}
语法将
to_string()
了
to_string()
方法的基础对象或用户对象放入字符串中。 这样可以避免不必要的字符串连接,并且通常看起来更加整洁:
var x = 3 var y = 4
Vector,Map,MapPair和所有原语也支持此功能。 向量以
[o1, o2, ...]
格式显示,映射为
[<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)
语言功能也可以设置呼叫条件(呼叫保护)。 如果不遵守,则会引发异常,否则将进行调用。 我还注意到,如果函数的末尾没有return语句,则将返回最后一个表达式。 小例程非常方便:
def div(x, y) : y != 0 { x / y }
类和Dynamic_Object
ChaiScript具有OOP的基础知识,如果您需要操作复杂的对象,这是绝对的优势。 该语言具有特殊类型-Dynamic_Object。 实际上,所有类和名称空间的实例都是具有预定义属性的Dynamic_Object。 动态对象允许您在脚本执行期间向其添加字段,然后访问它们:
var obj = Dynamic_Object(); obj.x = 3; obj.f = fun(arg) { print(this.x + arg); }
类的定义非常简单。 可以将它们设置为字段,方法,构造函数。 从有趣的
set_explicit(object, value)
通过特殊功能
set_explicit(object, value)
可以通过在类声明后禁止添加新方法或属性来“固定”对象的字段(通常在构造函数中完成):
class Widget { var id;
重要的一点-实际上,类方法只是函数,其第一个参数是具有明确指定类型的类的对象。 因此,以下代码等效于向现有类添加方法:
def set_id(Widget w, id) { w.id = id } w.set_id(9)
熟悉C#的任何人都可以痛苦地替换看起来像扩展方法的东西,并且会接近事实。 因此,使用该语言,您甚至可以为内置类(例如,字符串或int)添加新功能。 作者还提供了一种使运算符重载的棘手方法:要做到这一点,您需要用波浪号(`)包围运算符,如下例所示:
命名空间
说到ChaiScript中的名称空间,应该牢记这些本质上是始终在全局上下文中的类。 您可以使用
namespace(name)
函数创建它们,然后添加必要的函数和类。 默认情况下,没有该语言的库,但是您可以使用扩展来安装它们,我们将在稍后讨论。 通常,名称空间初始化可能看起来像这样:
namespace("math")
Lambda表达式和其他功能
ChaiScript中的Lambda表达式类似于我们从C ++知道的表达式。 他们使用
fun关键字,它们还需要显式指定捕获的变量,但是它们始终通过引用来执行。 该语言还具有绑定函数,可让您将值绑定到函数参数:
var func_object = fun(x) { x * x } func_object(9)
例外情况
在脚本执行过程中可能会发生异常。 它们可以在ChaiScript本身(我们将在这里讨论)和C ++中进行拦截。 语法与加号绝对相同,甚至可以抛出数字或字符串:
try { eval(x + 1)
以一种好的方式,您应该定义异常类并将其抛出。 在第二部分中,我们将讨论如何在C ++中对其进行拦截。 对于解释器异常,ChaiScript抛出其异常,例如eval_error,bad_boxed_cast等。
解释器常量
令我惊讶的是,该语言竟然是某种编译器宏-它们只有4个,它们全部用于标识上下文,并且主要用于错误处理:
错误陷阱
如果尚未声明要调用的函数,则会引发异常。 如果这对您来说不可接受,则可以定义一个特殊函数
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)
内建功能
ChaiScript定义了许多内置函数,在本文中,我想谈一谈特别有用的函数。 其中:
eval(str)
,
eval_file(filename)
,
to_json(object)
,
from_json(str)
:
var x = 3 var y = 5 var res = eval("x * y")
用C ++实现
安装方式
ChaiScript是一个基于C ++模板的仅标头库。 因此,对于安装,您只需要制作一个克隆
存储库或将所有来自
此文件夹的文件放入您的项目中。 由于根据IDE的不同,所有这些操作都有不同的方式,并且已经在论坛上进行了很长时间的详细描述,因此,我们假定您已成功连接该库,并编译了包含
#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对象声明为静态对象,则会遇到令人不愉快的运行时错误。 这是因为该语言默认情况下支持多线程,并存储在其析构函数中访问的本地流变量。 但是,它们在调用静态实例的析构函数之前已被销毁,因此,我们遇到访问冲突或分段错误错误。 基于
github上的
问题 ,最简单的解决方案是将
#define CHAISCRIPT_NO_THREADS
放在编译器设置中或在包含库文件之前,从而禁用多线程。 据我了解,无法修复此错误。
现在,我们将详细分析C ++和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);
如您所见,ChaiScript在它未知的C ++类中可以完全平静地工作,并且可以调用其方法。 如果您在代码中的某个地方犯了一个错误,则脚本很可能会
error in function dispatch
抛出异常类型的异常,这一点都不重要。 但是,不仅可以导入函数,而且让我们看看如何使用该库向脚本添加变量。 为此,请更难选择任务-import std :: vector <Widget>。
chaiscript::var
函数和
add_global
方法将帮助我们解决这一问题。 我们还将
Data
public字段添加到Widget中,以了解如何导入class字段:
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 #1 with data: 0
widget #2 with data: 2
widget #3 with data: 4
widget #2 with data: 2
, widget #3 with data: 4
。 我们在ChaiScript中添加了一个指向class字段的指针,并且由于该字段原来是原始类型,因此我们更改其值。 此外,还添加了几种方法来使用
std::vector
,包括
operator[]
。 那些熟悉STL的人知道
std::vector
两种索引方法-一种返回常量链接,另一种返回简单链接。 这就是为什么对于重载的函数,必须显式指示其类型-否则会引起歧义,并且编译器将引发错误。
该库提供了更多的添加对象的方法,但是它们几乎都是相同的,因此我看不到详细考虑它们的意义。 作为一个小提示,下面是下面的代码:
chai.add(chaiscript::var(x), "x");
使用STL容器
如果要将包含
原始类型的STL容器传递给ChaiScript,可以将模板容器实例化添加到脚本中,这样就不必为每种类型导入方法。
using MyVector = std::vector<std::pair<int, std::string>>; MyVector V; V.emplace_back(1, "John"); V.emplace_back(3, "Bob");
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;
ChaiScript , C++ , . - (, ), ,
std::vector<Widget>
.
. ChaiScript , , :
Widget w(3); w.Data = 4444;
«» 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;
因此,没有必要用C ++本身编写转换函数,只需将其导出到ChaiScript。您可以添加转换,并且已经在脚本代码本身中描述了新功能。如果两种类型的转换很简单,则可以将lambda作为参数传递给function type_conversion
。投射时会被调用。使用类似的原理将Vector或Map ChaiScript转换为您的自定义类型。为此,在库vector_conversion
中定义了map_conversion
。解压缩ChaiScript返回值
eval
eval_file
Boxed_Value
. C++, ,
boxed_cast<T>
. , ,
bad_boxed_cast
:
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"); } )");
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;
, 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. , :
, -, . :
chaiscript ImGui:, . , Lua , , (JIT ), ChaiScript . , , .
. C++ ( Lua ), ChaiScript . . .