C++ 的指令碼語言:ChaiScript

adam1q84發表於2019-05-10

ChaiScript 是一個可以方便的嵌在 C++ 程式裡的指令碼語言,相比於 V8(Google JavaScript)和 Lua 來說,它的用法要簡單得多。

ChaiScript 和 STL 一樣只有標頭檔案,缺點是編譯慢,而且因為大量使用模板,編譯就更慢。

說明

  • 本文示例程式碼一律假定已經 using namespace chaiscript

  • 本文已經有些年頭了,不代表 ChaiScript 最新特性。

函式

匯出(expose)函式到 ChaiScript:

int echo (int i) {
  return i;
}

ChaiScript chai;
chai.add(fun(&echo), "echo");  // 匯出到ChaiScript
int i = chai.eval<int>("echo(1)");  // 在ChaiScript裡呼叫這個函式

匯出過載函式時,需要指定具體型別:

string echo(const string& s) {
  return s;
}

ChaiScript chai;
chai.add(fun<int (int)>(&echo), "echo");
chai.add(fun<string (const string&)>(&echo), "echo");

int i = chai.eval<int>("echo(1)");
string s = chai.eval<string>("echo("string")");

強型別

ChaiScript is very strongly typed and does not do any int to unsigned int conversions automatically.
http://chaiscript.com/node/126

給定:

unsigned int fac(unsigned int n) {
  return n == 0 ? 1 : n * fac(n - 1);
}
chai.add(fun(&fac), "fac");

這樣呼叫是不行的:

unsigned int fac = chai.eval<unsigned int>("fac(3)"); // 錯!

會有 bad_boxed_cast 異常。應該在呼叫時顯式的轉成 unsigned int

unsigned int fac = chai.eval<unsigned int>("fac(unsigned_int(3))");

或者,直接以 signed int 來匯出這個函式:

chai.add(fun<int (int)>(&fac), "fac");
int fac = chai.eval<int>("fac(3)");

C++ 雖然也是強型別,但是允許 signedunsigned 之間隱式轉換。如果 fac 函式只有 unsigned 實現,fac(3) 也能呼叫,3signed3u 表示 unsigned),但是 C++ 可以將它隱式轉換成 unsigned。ChaiScript 則不行,signed 就是 signedunsigned 就是 unsigned,需要顯式轉換(3u 這種寫法 ChaiScript 也不支援)。

對 ChaiScript 來說,intdouble 都是 POD(plain object data)型別,boolstd::string 也是,完整的列表詳見 ChaiScript bootstrap(自舉,引導)的那段程式碼。

陣列

ChaiScript 提供的 Vector 類似於 STL 的 vector

var v := Vector()
v.push_back(1)
v.push_back(2)
print(v) // [1, 2]

Vector 的構造和初始化可以簡化(類似於 Python 的列表):

var v := [1, 2]

在 STL 的 vector 和 ChaiScript 的 Vector 之間,是不可以“直接”轉換的:

vector<int> ints = chai.eval<vector<int> >("[1, 2, 3]"); // 錯!

這樣會有 bad_boxed_cast 異常。右邊 eval 返回的是 vector<Boxed_Value>,不能自動轉成 vector<int>。也可以簡單理解成:自動 Box 和 Unbox 只對 POD 型別有效。這是一種設計上的折中吧,作者是這樣解釋的:

There`s no built in way to do conversions between typed vectors and vectors of Boxed_Value. We tried to implement that, but it introduced too much overhead in the code.
http://chaiscript.com/node/118

下面說說細節。首先是 eval 的結果可以引用:

vector<Boxed_Value>& ints = chai.eval<vector<Boxed_Value> >("[1, 2, 3]");

這就避免了一次拷貝構造。但是注意,Boxed_Value 裡具體的資料是引用計數的:

class Boxed_Value {
private:
  boost::shared_ptr<Data> m_data;
};

所以 vector 本身引用與否,不影響內部的資料。
假如想在 C++ 這邊改 ChaiScript 裡的這個陣列,也可以:

vector<Boxed_Value>& ints = chai.eval<vector<Boxed_Value> >("var a := [1, 2, 3]; a;"); // 宣告變數a方便後續訪問
ints[0].assign(Boxed_Value(4));
chai.eval("print(a[0])"); // 4

這裡有兩點值得注意。首先,變數 ints 不用引用也可以達到相同效果,因為如前所述,Boxed_Value 內部的資料有引用計數。其次,assign 不能替換成 =

ints[0] = Boxed_Value(4);

=,之前的那個 Boxed_Value 就被替換掉了。

引用

下面這兩種寫法的差別在於,第一種多一次拷貝構造:

var a = Vector()
var a := Vector()

看兩個例子。

例一:

var a := Vector()
var b = a // 拷貝構造了b
b.push_back(1) // b改了,a仍為[]

例二:

var a := Vector()
var b := a // b引用a
b.push_back(1) // a和b都改了

發現一個 bug,用快捷方式建立的 Vector,不能再被其他變數引用:

var a := [1, 2, 3] // 用快捷方式建立
var b := a
b.push_back(1) // Crash!

可見 ChaiScript 還有很多問題。不夠成熟是我對它最大的顧慮。

class Buffer {
public:
  size_t LineCount() const { return lines_.size(); }

  string& GetLine(size_t line_index) {
    return lines_[line_index];
  }
  const string& GetLine(size_t line_index) const {
    return lines_[line_index];
  }

  void AddLine(const string& line) {
    lines_.push_back(line);
  }

private:
  vector<string> lines_;
};
chai.add(user_type<Buffer>(), "Buffer");
// Default constructor
chai.add(constructor<Buffer ()>(), "Buffer");
// Copy constructor
chai.add(constructor<Buffer (const Buffer &)>(), "Buffer");

chai.add(fun(&Buffer::AddLine), "AddLine");
chai.add(fun(&Buffer::LineCount), "LineCount");

成員變數

成員變數的匯出方法和成員函式相同。

class Option {
public:
  std::string cjk;
  std::string file_encoding;
  bool show_space;
  bool show_number;
};
chai_->add(user_type<Option>(), "Option");

chai_->add(fun(&Option::cjk), "cjk");
chai_->add(fun(&Option::file_encoding), "file_encoding");
chai_->add(fun(&Option::show_number), "show_number");
chai_->add(fun(&Option::show_space), "show_space");

一些補充

傳值,傳引用

  1. Basic data types are passed by value.

  2. Class data types are passed by reference.

  3. There is not specific syntax for one or the other.

  4. Use wrapper classes for basic data types, if you want to pass them by reference (like Java.)

關於 use

如果匯出一個變數,然後 eval 一個檔案,這個檔案又 use 了另一個檔案,那麼被 use 的那個檔案是看不到這個變數的。

use 就相當於 C++ 的 include,被 use 的檔案一般定義了一些公共的函式和類。

相關文章