Squirrel/squallとは
SquirrelのC++11バインディング「squall」を作ってみました。
Squirrelについてご存じない方のために説明しますと、概ね「luaの置き換えを狙った組み込み言語」です。文法は「クラスのあるJavascript」といった趣で、スクリプト側から見るとあまり癖がない印象ですが、コルーチンなどはあります。
現状luaの牙城を崩せてはいない感じなのでマイナー言語だと思ったのですが、市販ソフトも含めてそれなりに使われているようなので、マイナー言語Advent Calenderの去年の方針からすると当てはまらなかったかもしれません。本も出てますし。
とはいえ、C++11の機能を利用したバインダの実装が割とコンパクトに纏まった気がするので、今後似たような組み込みスクリプト言語が現れたら、そのC++11バインディングを作るのに多少参考にしてもらえるのではないか、ということでAdvent Calenderに登録させていただきました。
Squirrelの組み込み
SquirrelをC(組み込む)側から見ると、だいたいluaと同じようなインターフェイスでスタックマシンをあれこれしてスクリプトを呼び出したり呼び出されたりするようになっています。ただこのスタックマシン操作がものすごくメンドクサくて、例えばSquirrel関数を呼び出すために引数を1個ずつプッシュするといったような操作を求められるので、適当なバインダーが必要になってきます。
SquirrelのC++バインディングは、わざわざ私が作らなくてもすでにいくつか存在するのですが、どれもC++03時代に作られていて、本来ナマの仮想マシンには存在しないはずの制限が加わっていたりするので、今回あえてC++11全開(当社比)で作りなおしてみました。既存のものと比較すると、グローバルな状態が存在しないクリーンな設計が特徴となっております。まだ実務で使ってみたことがないので、実用に耐えるかはわかりませんが……
レポジトリはこちらです。
https://github.com/jonigata/squall
squallの雰囲気
例えば、C++側でラムダ式を作ってそれをSquirrelにグローバル関数として登録する場合、このような感じで記述できます。
#include "squall/squall_vmstd.hpp"
#include <iostream>
int main() {
squall::VMStd vm;
vm.defun("bar", [](int x)->int {
std::cout << "**** lambda: " << x << std::endl;
return 7777;
});
return 0;
}
実装するかどうか迷ってる機能
Squirrelではクロージャを使えるので、その気になればstd::function <-> Squirrelクロージャの相互変換をサポートすることも可能なのですが、embedでクロージャの交換をしたがる人なんているのかなという疑問が晴れないので、今のところ実装する気がないです。めっちゃ使います!!! という人がいたらやりますが、どっちかというとPullRequestがほしいです。 cocos2dに組み込んでたら自分が必要になったので対応しました。
コルーチン対応は実装しました。
実装で難しかったところ
※マイナー言語に関する話題はここで終わりです、ごめんなさい。これからはC++11に関する話です。といっても、これから組み込み言語を作ろうという方にはそれなりに参考にしてもらえる話ではないかと思います。
とにかく、関数登録するのに任意のCallableオブジェクトを渡すのが困難でした。
C++には、関数として呼び出せるものに「関数ポインタ」「std::function」「ラムダ」「operator()をオーバーロードしたオブジェクト」などがあるのですが、それらをすべて受け取れるようなtemplate関数をつくろうとする場合、こういうことができそうでできません。
struct Foo {
void operator()(int) {}
};
template <class F>
void bar(F f) {
std::function<型がわからない!> ff = f;
}
int main() {
bar([](int)->void { });
bar(Foo());
}
Callableのシグネチャを得るfunction_traits<F>::signature
みたいなものが標準であるかな、と思っていたら、どうもない(らしい)のです。なんでないのかな~と思いつつ調べたりしながら作っていたところ、なんとなく理由がわかりました。
struct Foo {
void operator()(int) {}
void operator()(float, bool) {}
};
template <class F>
void bar(F f) {
using sig = std::function_traits<F>::signature; // どっちのオーバーロード?
std::function<sig> ff = f;
}
int main() {
bar(Foo());
}
このように、関数風オブジェクト(C++用語ではなぜかファンクタといいます)のシグネチャは一意に決定できないからです。
まあ、そのうち、こういうときはvariadic templateみたいにpackするみたいな仕様が生まれてfunction_traitsが制定されたりするかもしれませんが、現状はオーバーロードはないものとして自作するしかないようです。
結果的には、このような形になりました。https://github.com/jonigata/squall/blob/master/squall/squall_make_function.hpp そんなにTMPに慣れているわけではないので、おかしいところがあったら教えていただけるとありがたいです。