Squirrel
- この記事は http://wikiwiki.jp/lua/?Squirrel に移行予定です。
SquirrelとLuaの比較
http://wiki.squirrel-lang.org/default.aspx/SquirrelWiki/LuaComparedToSquirrel.htmlより
- Luaの文法と較べると、Squirrelの文法のほうがよりC/C++/C#に近い (私見だが、Luaの文法には特に長所がないように思う)
- クラスとインスタンスはSquirrelに組み込まれている (Luaではテーブルでシミュレートしている)
- Squirrelは(高速な)配列を持つ
- Squirrelは経験の豊富な商業ゲーム開発者が、商業ゲームの作成で使用するために開発された (Luaでうまく動作するものを取り入れ、そうでない要素を変更したり捨てたりした)。 Luaからテーブルと文字列コードを取り入れたが、言語のそれ以外は完全に新規である。
- 多くの開発者は単純なテンプレートバインディングを好む。 LuaにとってLuaPlusは優秀であるが、クラス/インスタンスを支援するための簡単なソリューションを提供しない。 luabindは非常に強力であり、クラス/インスタンスを支援にも優れているが、設定やコンパイルもより複雑になっており、コンパイル時間も長くなり、デバッグビルドでは長大なシンボルテーブルが生じてしまう。そのような複雑なサポートが必要なアプリケーションもあるが、ゲームには常に必要であったり望まれていたりはしない。 Squaddは、次のようなバインディングがある。
- DxSquirrel (単純なマクロによるバインディングシステム)、
- Squadd (boostベースのluabind風の機能)、
- SqPlus (商用ゲーム開発に必要なすべてを提供し、単純で簡潔なクラス/関数/変数/定数バインディング、C/C++からのスクリプト関数呼び出し、非常に軽量なテンプレートコードを含む)
- Squirrelは優れた、文法をハイライトする、ポータブルなリモートデバッガを持つ (Eclipse環境を使用する場合)
- (Squirrelの作者である) Albertoは、FarCryのためのスクリプト開発中にLuaを使用していてぶつかった問題は、インクリメンタルGCでは解決しなかったと言っている。いずれにせよ、Lua 5.1はまだリリースされていない (現在アルファ版)。 LuaのGC問題が解決されたとしても、私にとってSquirrelの言語と文法のほうがLuaのものより好ましい (私は2000年頃からLua 4.xを使用しており、ゲームのための優れたスクリプト言語としてLuaを薦めていた。商用ゲームのための初期の採用例としてはLucasArtsを使用していた)。
- (Luaのように) 代入を使用するとき、新規変数を偶然に作成することがない。
- "clinet = 1" は、"clinet" (clientのタイプミス)が存在しない場合は、エラーとなる。新規変数を作成してそれに代入するには "client <- 1" と書く必要がある。
- SqPlusは VS.NET 2003 (VS8互換) のプロジェクトとソリューションである。ダウンロードして解凍し、コンパイルして実行するのに5分もかからない。
C++からのSquirrel関数呼び出し
http://wiki.squirrel-lang.org/default.aspx/SquirrelWiki/EmbeddingGettingStarted.html
次のようなSquirrelコードがあるとする。
function foo(i, f, s) {
print("Called foo(), i=" + i + ", f=" + f + ", s='" + s + "'");
}
この関数を呼び出すCのコードは次のようなものになる。
#include <stdarg.h>
#include <stdio.h>
#include <squirrel.h>
#include <sqstdio.h>
#include <sqstdaux.h>
void printfunc(HSQUIRRELVM v, const SQChar* s, ...) {
va_list arglist;
va_start(arglist, s);
vprintf(s, arglist);
va_end(arglist);
}
void call_foo(HSQUIRRELVM v, int n, float f, const SQChar* s) {
int top = sq_gettop(v); // 呼び出し前のスタックサイズを保存
sq_pushroottable(v); // ルートテーブルをpush
sq_pushstring(v, _SC("foo"), -1);
if (SQ_SUCCEEDED(sq_get(v, -2))) { // フィールド"foo"をルートテーブルから取得
sq_pushroottable(v); // "this"をpush (この例ではルートテーブルに)
sq_pushinteger(v, n);
sq_pushfloat(v, f);
sq_pushstring(v, s, -1);
sq_call(v, 4, 0); // 関数を呼ぶ
}
sq_settop(v, top); // 元のスタックサイズに戻す
}
int main(int argc, char** argv) {
HSQUIRRELVM v;
v = sq_open(1024); // VMを初期スタックサイズを1024で作成
sqstd_seterrorhandlers(v);
sq_setprintfunc(v, printfunc); // 表示用関数をセット
sq_pushroottable(v); // ルートテーブルをpush
if (SQ_SUCCEEDED(sqstd_dofile(v, _SC("test.nut"), 0, 1))) { // 文法エラーがあるならそれも表示
call_foo(v, 1, 2.5, _SC("teststring"));
}
sq_pop(v, 1); // ルートテーブルを取り除く
sq_close(v);
return 0;
}
このコードを詳しく見ていく。
Squirrelの初期化
最初にすべきことは、仮想マシン(VM)を作成することである。
HSQUIRRELVM v = sq_open(1024);
1024は初期スタックサイズである。Squirrelではスタックは自動的に増加するため、この値はVMへの単なるヒントでしかない。
Squirrelでは自作のエラー処理関数を定義することができる。この例では標準I/Oライブラリ上で用意されたものを使用する。
sqstd_seterrorhandlers(v);
print()のような関数において、なんらかのテキストを出力させるようにするため、VMにプリント関数を提供する必要がある。
sq_setprintfunc(v, printfunc);
ここで、printfuncは次のように定義する。
void printfunc(HSQUIRRELVM v, const SQChar* s, ...) {
va_list arglist;
va_start(arglist, s);
scvprintf(s, arglist);
va_end(arglist);
}
スクリプトの実行
Squirrelは、あらゆる種類のメディアからスクリプトをコンパイルと実行するための低レベルAPIを提供している。しかし、スクリプトは単純なテキストファイルであり、それをコンパイルしてすぐに実行したい場合がほとんどである。これを達成するのにもっとも簡単な方法は標準I/Oライブラリ(sqstdio.h)を使用することである。
sqstd_dofileはスクリプトをコンパイルして実行する。
sq_pushroottable(v); sqstd_dofile(v, "myscript.nut",0);
sqstd_dofileではスタックトップに、そのスクリプト実行中に使用されるべきオブジェクトがひとつ存在する必要がある(この例ではルートテーブル)。
C++からのSquirrel関数の呼び出し
この例で用意したSquirrelスクリプトでは関数fooを定義した。その関数を呼び出すために、まず、それを見つけてスタック上に配置する必要がある。この関数はグローバルスコープ上に宣言されているため、これはルートテーブルに保存されている。
void call_foo(HSQUIRRELVM v, int n, float f, const SQChar* s) {
int top = sq_gettop(v);
sq_pushroottable(v);
sq_pushstring(v, _SC("foo"), -1);
if (SQ_SUCCEEDED(sq_get(v, -2))) {
sq_pushroottable(v);
sq_pushinteger(v, n);
sq_pushfloat(v, f);
sq_pushstring(v, s, -1);
sq_call(v, 4, 0);
}
sq_settop(v, top);
}
call_fooはfoo関数を1つの整数と1つの浮動小数点数と1つの文字列を引数としてfooを呼び出す。
これを1行ずつ見ていく。まず、呼び出した後の処理のために、現在のスタックサイズを保存しておく。
int top = sq_gettop(v);
次に、(フェッチしたい関数が存在する)ルートテーブルをpushする。
sq_pushroottable(v);
このときのスタックの状態を次に示す。
| negative index | object |
|---|---|
| -1 | root table |
| ... | ... |
フェッチしたい関数を含んでいるスロット名である文字列(この例では"foo")をpushする。
sq_pushstring(v, _SC("foo"), -1);
引数の-1はVMが自動的に文字列長を計算することを指定している。
| negative index | object |
|---|---|
| -1(top) | "foo" |
| -2 | root table |
| ... | ... |
その後、関数をフェッチする。
if (SQ_SUCCEEDED(sq_get(v, -2))) {
関数sq_getはスタックからオブジェクトをpopする(この例では"foo")。それをキーとして、スタック(この例ではルートテーブル)のトップからの位置(この例では-2)にあるオブジェクトをフェッチする。この関数が成功したら、結果をスタックにpushする。
sq_get後のスタックの状態を次に示す。
| negative index | object |
|---|---|
| -1(top) | function foo(){} |
| -2 | root table |
| ... | ... |
次に、関数の引数をスタック上に積んでいく。このとき、引数としてルートテーブルを再びpushする必要がある。(ちょうどC++のクラスメンバのように)Squirrelの関数は常に隠し引数としてthisを持つためである。
sq_pushroottable(v); sq_pushinteger(v, n); sq_pushfloat(v, f); sq_pushstring(v, s, -1);
| negative index | object |
|---|---|
| -1(top) | string |
| -2 | float |
| -3 | integer |
| -4 | root table |
| -5 | function foo(){} |
| -6 | root table |
| ... | ... |
最終的に関数sq_callが呼ばれる。これは実際に関数fooを呼び出して実行する。
sq_call(v, 4, 0);
sq_callの第2引数である4は、この関数が4つの引数(this、整数、浮動小数点数、文字列)を持つことを意味する。Squirrelはこの4引数をpopしたあとのスタックのトップが関数であるとみなす。sq_callの第3引数は、関数の戻り値の数を指定する。この例では0なので、この関数が戻り値を持たないことを意味する。
sq_call後のスタックの状態を次に示す。
| negative index | object |
|---|---|
| -1(top) | function foo(){} |
| -2 | root table |
| ... | ... |
関数を実行したので、スタックを初期状態にリセットする。
sq_settop(v,top);
後処理
sqstd_dofileを実行するために、先ほどpushしたルートテーブルはこの時点で削除することができる。
sq_pop(v,1);
プログラムの終わりで、SquirrelのVMを削除する。
sq_close(v);
これですべての割り当てられていたリソースが解放される。
SquirrelからのC++関数呼び出し
http://wiki.squirrel-lang.org/default.aspx/SquirrelWiki/SquirrelCallToCpp.html
まず、いくつかのヘルパ関数を用意し、次にメインコードを示す。
コールバック関数
Squirrelから呼び出したい関数は、次のような形式でなければならない(fnが呼び出したい関数)。
SQInteger fn (HSQUIRRELVM v)
この例では次のような関数を用いる。
SQInteger print_args(HSQUIRRELVM v) {
SQInteger nargs = sq_gettop(v); // 引数の数
for (SQInteger n = 1; n <= nargs; n++) {
printf("arg %d is ",n);
switch(sq_gettype(v,n)) {
case OT_NULL:
printf("null");
break;
case OT_INTEGER:
printf("integer");
break;
case OT_FLOAT:
printf("float");
break;
case OT_STRING:
printf("string");
break;
case OT_TABLE:
printf("table");
break;
case OT_ARRAY:
printf("array");
break;
case OT_USERDATA:
printf("userdata");
break;
case OT_CLOSURE:
printf("closure(function)");
break;
case OT_NATIVECLOSURE:
printf("native closure(C function)");
break;
case OT_GENERATOR:
printf("generator");
break;
case OT_USERPOINTER:
printf("userpointer");
break;
default:
return sq_throwerror(v,"invalid param"); // 例外をスロー
}
printf("\n");
}
sq_pushinteger(v, nargs); // 戻り値として、引数の数をpush
return 1; // 戻り値の数が1なので1
}
関数登録
次の関数はグローバル関数を登録するためのヘルパ関数である(付属の文書より)。
SQInteger register_global_func(HSQUIRRELVM v, SQFUNCTION f, const char* fname) {
sq_pushroottable(v);
sq_pushstring(v, fname, -1);
sq_newclosure(v, f, 0); // 新しい関数を作成
sq_createslot(v, -3);
sq_pop(v, 1);
return 0;
}
スクリプト関数実行
void run_script(HSQUIRRELVM v, char* cmd) {
sq_compilebuffer(v, cmd, (int)strlen(cmd)*sizeof(SQChar), "compile", 1);
sq_pushroottable(v);
sq_call(v, 1, 1);
}
メインコード
HSQUIRRELVM sqvm = sq_open(1024); register_global_func(sqvm, ::print_args, "prt"); char* cmd = "prt (100);"; run_script(sqvm, cmd);
実行結果
Arg 1 is table Arg 2 is integer
この例の関数print_argsは、Squirrelにおいては"prt"として登録されている。メインコードにおいて、Squirrelスクリプトがこの関数を1引数で呼び出しているが、複数の引数を取ることもできる。
いろいろ
local宣言
次のコードはエラーとならない。
local x = 3 ... local x = 9
配列の添字
Squirrelでは真偽値型は整数値化できないので、配列の添字などにtrueなどを使用できない。しかし、浮動小数点型を直接使用することができる。
local a = array(10);
a[1] = 100;
a[1.5] = 99; // Cではエラー
// a[true] = 99; // Squirrelではエラー
foreach (i, j in a)
::print(i + " " + j + "\n");
bindenv
例えば次のようなコードがある。
class Foo {
val = 0;
constructor(v) { val = v; }
function f() { ::print("Foo " + val + "\n"); }
}
local foo = Foo(3);
foo.f();
で、このFoo.f()を呼び出したいので次のように書きたいときがある。
class Bar {
func = null;
constructor(f) { func = f; }
function g() { func(); }
}
local bar = Bar(foo.f);
bar.f();
しかし、これはエラーとなる。
AN ERROR HAS OCCURED [the index 'val' does not exist] CALLSTACK *FUNCTION [f()] hoge line [6] *FUNCTION [g()] hoge line [12] *FUNCTION [main()] hoge line [20]
f()が呼ばれたとき、そのときのthisはbarとなってしまうからである(だから、Bar内にval="foobar"と書けばエラーとならずにfoobarが表示される)。
thisがfooとなるようにするには、次のようにあらかじめbindenvで束縛しておく必要がある。
local bar = Bar(foo.f.bindenv(foo));
他にも、自分でthisを保持する方法もある。
class Buzz {
obj = null;
func = null;
constructor(o, f) { obj = o; func = f; }
function h() { func.call(obj); }
}
local buzz = Buzz(foo, foo.f)
buzz.h();
参考文献:
http://squirrel-lang.org/forums/thread/1339.aspx
その他
Singleton
Squirrelで(通常の意味での)Singletonは使用できない。C++やJavaと異なり、Squirrelではクラス変数が常に変更可能ではないからだ。
例えば、次のコードを見てみよう。
class Foo {
constructor() {}
static bar = null;
static function init() {
Foo.bar <- Foo();
}
}
Foo.init();
これは、エラーとなる。
AN ERROR HAS OCCURED [trying to modify a class that has already been instantiated(既にインスタンス化されたクラスを修正しようとした)]
Foo.init()は次のような手順で実行される
- Fooインスタンスを作成する
- インスタンスをFoo.barに代入する
しかし、Step 1の時点でクラスFooのインスタンスが作成されている。Squirrelでは、インスタンスがひとつでも作成された時点で、インタンス変数は追加できなくなり、クラス変数は変更できなくなる。そのため、Step 2の代入がエラーとなってしまう。
これを回避するには、いくつかの方法が考えられる。
別インスタンスを介する方法
ホルダークラスを介して初期化を行う。これはメタメソッドを使用しない限りは常にアクセスが冗長になってしまう。
class InstanceHolder {
instance = null;
}
class Foo {
constructor() {}
static bar = InstanceHolder();
static function init() {
Foo.bar.instance = Foo();
}
}
Foo.init();
monostate
Singletonとは意味が異なってしまうが、monostate風にグローバル上にテーブルを作る方法も考えられる。
global <- {
foo = null
}
global.foo = Foo();
定数の作成
インスタンスがひとつでも作成された時点で、クラス変数は変更できなくなる。これを利用すると、定数が使えるようになる。
class Constants {
constructor() {}
static pi = 3.1415;
static foo = null; // freeze()するまでに代入が必要な変数
static function freeze() {
Constants();
}
}
Constants.foo = Foo();
Constants.freeze();
freeze()は無駄にインスタンスを作成しているだけだが、これによってクラス変数が変更できなくなる。
C++オブジェクトでのメタメソッド使用
Squirrelから、C++オブジェクトの生成と、破棄を行う。ついでにC++オブジェクトにメタメソッドを書いておく。
例えば、使用したいC++オブジェクトは次のような構造体とする。
struct Foobar {
int foo;
int bar;
};
これを使用するためには、次のような関数foobar_register_foobarlibを作成する必要がある。この関数をsq_init()の直後あたりに呼び出すようにする。
SQRESULT foobar_register_foobarlib(HSQUIRRELVM v) {
sq_pushstring(v, _SC("Foobar"), -1);
sq_newclass(v, SQFalse);
sq_createslot(v, -3);
}
クラスに関数を追加する
しかし、これだけでは何の役にも立たないクラスを追加しただけにすぎない。なぜなら、先ほどのコードをSquirrelコードにすると次のようなものになるからだ。
class Foobar {}
Foobarに関数を追加するには、ルートテーブル上のスロットFoobarにクラスを作成する(sq_createslot(v, -3)のこと)前に、クラスFoobar上のスロットに関数を作成する必要がある。
これは、基本的には先ほどのクラス追加や「SquirrelからのC++関数呼び出し」と同様の手順をとる。異なる点は、スロットの追加する対象がFoobarであることである。また、ネイティブクロージャを呼び出すときにはsq_setparamscheckとsq_setnativeclosurenameを用いなければならない。
この部分は若干面倒なのだが、Squirrel内で用いられている#define文を利用すると、多少楽になる。では、実際にクラスFoobarにコンストラクタを追加してみよう。
#define _DECL_FUNC(name,nparams,tycheck) {_SC(#name),foobar_##name,nparams,tycheck}
static SQRegFunction foobarlib_funcs[] = {
_DECL_FUNC(constructor, 1, _SC(".")),
{0,0}
};
SQRESULT foobar_register_foobarlib(HSQUIRRELVM v) {
sq_pushstring(v, _SC("Foobar"), -1);
sq_newclass(v, SQFalse);
for (SQInteger i = 0; foobarlib_funcs[i].name != 0; ++i) {
sq_pushstring(v, foobarlib_funcs[i].name, -1);
sq_newclosure(v, foobarlib_funcs[i].f, 0);
sq_setparamscheck(v, foobarlib_funcs[i].nparamscheck, foobarlib_funcs[i].typemask);
sq_setnativeclosurename(v, -1, foobarlib_funcs[i].name);
sq_createslot(v, -3);
}
sq_createslot(v, -3);
}
関数を追加するには、foobarlib_funcsに項目を追加していくだけで済む。
最後に、Squirrel側から呼び出すコンストラクタを作成する。#define文によってコンストラクタのための関数名はfoobar_constructorとなる。
static SQInteger foobar_constructor(HSQUIRRELVM v) {
Foobar* obj = new Foobar;
obj->foo = 0;
obj->bar = 0;
sq_setinstanceup(v, 1, obj);
sq_setreleasehook(v, 1, foobar_releasehook);
return 1;
}
なお、コンストラクタで参照カウンタが0になったときに呼ばれる破棄用の関数を登録しておくとよい。
static SQInteger foobar_releasehook(SQUserPointer p, SQInteger size) {
delete (Foobar*) p;
return 1;
}
ここまでの作業で、SquirrelからFoobarの生成と破棄ができるようになった。
クラスにメタメソッドを追加する
foobar.c
struct Foobar {
int foo;
int bar;
};
static SQInteger foobar_releasehook(SQUserPointer p, SQInteger size) {
delete (Foobar*) p;
return 1;
}
static SQInteger foobar_constructor(HSQUIRRELVM v) {
Foobar* obj = new Foobar;
obj->foo = 0;
obj->bar = 0;
sq_setinstanceup(v, 1, obj);
//sq_setreleasehook(v, 1, foobar_releasehook);
return 1;
}
static SQInteger foobar__set(HSQUIRRELVM v) {
const SQChar* key; sq_getstring(v, 2, &key);
Foobar* obj;
sq_getinstanceup(v, 1, (SQUserPointer *)&obj, 0);
if ( ! strcmp(key, "foo")) {
SQInteger val; sq_getinteger(v, 3, &val);
obj->foo = val;
} else if ( ! strcmp(key, "bar")) {
SQInteger val; sq_getinteger(v, 3, &val);
obj->bar = val;
} else {
return sq_throwerror(v, "invalid param");
}
return 0;
}
static SQInteger foobar__get(HSQUIRRELVM v) {
const SQChar* key; sq_getstring(v, 2, &key);
Foobar* obj;
sq_getinstanceup(v, 1, (SQUserPointer *)&obj, 0);
if ( ! strcmp(key, "foo")) {
sq_pushinteger(v, obj->foo);
} else if ( ! strcmp(key, "bar")) {
sq_pushinteger(v, obj->bar);
} else {
return sq_throwerror(v, "invalid param");
}
return 1;
}
static SQInteger foobar__nexti(HSQUIRRELVM v) {
if (sq_gettype(v, 2) == OT_NULL) { // first idx
sq_pushstring(v, _SC("foo"), -1);
return 1;
}
const SQChar* prev; sq_getstring(v, 2, &prev);
if ( ! strcmp(prev, "foo")) {
sq_pushstring(v, _SC("bar"), -1);
} else if ( ! strcmp(prev, "bar")) {
sq_pushnull(v);
} else {
return sq_throwerror(v,"invalid param");
}
return 1;
}
#define _DECL_FUNC(name,nparams,tycheck) {_SC(#name),foobar_##name,nparams,tycheck}
static SQRegFunction foobarlib_funcs[] = {
_DECL_FUNC(constructor, 1, _SC(".")),
_DECL_FUNC(_set, 3, _SC("xs.")),
_DECL_FUNC(_get, 2, _SC("xs")),
_DECL_FUNC(_nexti, 2, _SC("xs|o")),
{0,0}
};
SQRESULT foobar_register_foobarlib(HSQUIRRELVM v) {
sq_pushstring(v, _SC("Foobar"), -1);
sq_newclass(v, SQFalse);
for (SQInteger i = 0; foobarlib_funcs[i].name != 0; ++i) {
sq_pushstring(v, foobarlib_funcs[i].name, -1);
sq_newclosure(v, foobarlib_funcs[i].f, 0);
sq_setparamscheck(v, foobarlib_funcs[i].nparamscheck, foobarlib_funcs[i].typemask);
sq_setnativeclosurename(v, -1, foobarlib_funcs[i].name);
sq_createslot(v, -3);
}
sq_createslot(v, -3);
}
sample.nut
local x = Foobar();
x.foo = 50
x.bar = 100
foreach (i, j in x) {
print("**" + i + " " + j + "\n");
}
(Last modified: 2008-02-03 15:56:48) [validate]