Squirrel_Lang/ch3

Last-modified: 2011-03-01 (火) 19:25:23

第3章 Embedding Squirrel

この章では、ホストアプリケーションへSquirrelを組み込む方法について述べる。
この章を理解するためにはC言語の知識が必要である。

拡張言語としての性質により、Squirrelのコンパイラと仮想マシン(VM)はCライブラリとして実装されている。
このライブラリにはスクリプトのコンパイル、関数の呼び出し、データの操作、VMの拡張を行うための関数が含まれている。
この言語をアプリケーションに組み込むために必要な宣言は、ヘッダファイルの"squirrel.h"のみである。

メモリ管理

Squirrelは参照カウント(RC)をメモリ管理の基本システムとして用い、
要求に応じて補助的にマークアンドスイープ・ガベージコレクタ(GC)を呼び出す。

ふたつのコンパイル時オプションがある。

  • RCとGCを用いる (デフォルト)

    ホストアプリケーションでは関数sq_collectgarbage()を呼ぶことができ、実行時にGCサイクルを行うことができる。
    GCはホストアプリケーションから明示的に呼ばれない限り、VMによって実行されない。
  • RCのみを用いる (#define NO_GARBAGE_COLLECTOR)

    この場合、VMが循環参照を検出することは不可能である。よって、メモリリークを回避するために、プログラマが明示的にこれを解決する必要がある。

2番目のオプションを用いる唯一の利点は、初期設定のGC付きのものと較べて、
各オプジェクトを保存するのためのポインタを2つ分(32ビットシステムでは8バイト)節約できることである。
これはテーブル、配列、関数、スレッド、ユーザデータ、ジェネレータに影響する(他には影響を与えない)。
このオプションは実行速度には影響を与えない。

Unicode

デフォルトではSquirrelの文字列はプレーン8ビットASCII文字となる。
しかし、VMにシンボル'_UNICODE'がdefineされているなら、コンパイラとAPIで文字が16ビットとして扱われる。

64ビットアーキテクチャ

SquirrelはC++プリプロセッサ上で'_SQ64'をdefineすることで64ビットアーキテクチャとしてコンパイルすることができる。
このフラグはsquirrel.hをincludeするすべてのプロジェクトでdefineされるべきである。

エラーの慣習

ほとんどのAPI関数はSQRESULTの値を返す。SQRESULTは関数が完全に成功したかどうかを示す。
マクロSQ_SUCCEEDED()とSQ_FAILED()は関数の結果をテストするのに使用される。

if (SQ_FAILED(sq_getstring(v, -1, &s)))
printf("getstring failed");

Squirrelの初期化

ホストアプリケーションが最初にすべきことはVMを作成することである。
関数sq_open()によって、ホストアプリケーションは任意の数のVMを作成することができる。

それぞれのVMは、必要でなくなったときに関数sq_close()を用いて解放させる必要がある。

int main(int argc, char** argv) {
	 HSQUIRRELVM v = sq_open(1024); // VMを初期スタックサイズ1024で作成
	 // ここでSquirrelを用いた処理を行う
	 sq_close(v);
}

スタック

Squirrelはスタックを通して、VMの持つ値を変更する。
この仕組みはプログラミング言語Luaから継承されている。
例えば、Squirrelの関数をC言語から呼ぶためには、関数や引数をスタックにpushし、それから関数を呼ぶ必要がある。
また同様に、SquirrelからC言語の関数を呼ぶためには、引数をスタックに積む必要がある。

スタックのインデックス

多くのAPI関数はインデックスによって、スタックの任意の要素を参照することができる。
スタックのインデックスは次のような慣習に従う:

  • スタックベースは1である
  • 負のインデックスはスタックトップからのオフセットを意味する。例えば、-1はスタックトップとなる
  • 0は不正なインデックスである

例を次に示す。この表はVMのスタックである。

スタック正のインデックス負のインデックス
"test"4-1(トップ)
13-2
0.52-3
"foo"1(ベース)-4

上の例では関数sq_gettopは4を返す。

スタックの操作

Squirrelスタック上のデータのpushと探索のために、API関数がいくつか提供されている。

既に存在している値をスタックのトップに置き直すためには、次の関数を使用する。

void sq_push(HSQUIRRELVM v, SQInteger idx);

任意の数の要素をpopするためには、次の関数を使用する。

void sq_pop(HSQUIRRELVM v, SQInteger nelemstopop);

スタックから要素を削除するには、次の関数を使用する。

void sq_remove(HSQUIRRELVM v, SQInteger idx);

To retrieve the top index (and size) of the current virtual stack you must call sq_gettop

SQInteger sq_gettop(HSQUIRRELVM v);

スタックのサイズを指定した値だけ確保させるには、関数sq_settopを使用する。

void sq_settop(HSQUIRRELVM v, SQInteger newtop);

newtopが以前の値よりも大きいなら、スタックの新しい箇所には値としてnullが与えられる。

次の関数はC言語の値をスタックへpushする。

void sq_pushstring(HSQUIRRELVM v, const SQChar *s, SQInteger len);
void sq_pushfloat(HSQUIRRELVM v, SQFloat f);
void sq_pushinteger(HSQUIRRELVM v, SQInteger n);
void sq_pushuserpointer(HSQUIRRELVM v, SQUserPointer p);
void sq_pushbool(HSQUIRRELVM v, SQBool b);

次の関数はスタックへnullをpushする。

void sq_pushnull(HSQUIRRELVM v);

スタックの任意の位置の値の型を知るには、次の関数を使用する。

SQObjectType sq_gettype(HSQUIRRELVM v, SQInteger idx);

この結果は次のうちのどれかになる。

OT_NULL, OT_INTEGER, OT_FLOAT, OT_STRING, OT_TABLE, OT_ARRAY, OT_USERDATA,
OT_CLOSURE, OT_NATIVECLOSURE, OT_GENERATOR, OT_USERPOINTER, OT_BOOL, OT_INSTANCE, OT_CLASS, OT_WEAKREF

次の関数はスタック上のSquirrelの値をC言語の値へと変換する。

SQRESULT sq_getstring(HSQUIRRELVM v, SQInteger idx, const SQChar **c);
SQRESULT sq_getinteger(HSQUIRRELVM v, SQInteger idx, SQInteger *i);
SQRESULT sq_getfloat(HSQUIRRELVM v, SQInteger idx, SQFloat *f);
SQRESULT sq_getuserpointer(HSQUIRRELVM v, SQInteger idx, SQUserPointer *p);
SQRESULT sq_getuserdata(HSQUIRRELVM v, SQInteger idx, SQUserPointer *p, SQUserPointer *typetag);
SQRESULT sq_getbool(HSQUIRRELVM v, SQInteger idx, SQBool *p);

関数sq_cmpはスタックから2つの値をpopし、その2つの値の(ANSI Cのstrcmpのような)関係を返す。

SQInteger sq_cmp(HSQUIRRELVM v);

実行時エラー処理

Squirrelのコード上で(try/catchを用いて)例外が扱われていないとき、実行時エラーが発生して現在のプログラムの実行は中断される。
コールバック関数をセットして、ホストプログラムから実行時エラーを横取りすることが可能である。これは、スクリプト作成者に意味のあるエラーを表示させたり、ビジュアルデバッガを実装するために有用である。

次のAPIの呼び出しは、スタックからひとつのSquirrel関数をpopし、それをエラーハンドラとしてセットする。

SQUIRREL_API void sq_seterrorhandler(HSQUIRRELVM v);

エラーハンドラは環境オブジェクト(this)ともうひとつのオブジェクト(これは任意のSquirrelの型が可能)の2引数で呼び出される。

スクリプトのコンパイル

関数sq_compileを用いることで、Squirrelスクリプトのコンパイルを行うことができる。

typedef SQInteger (*SQLEXREADFUNC)(SQUserPointer userdata);
SQRESULT sq_compile(HSQUIRRELVM v, SQREADFUNC read,SQUserPointer p,
	 const SQChar *sourcename, SQBool raiseerror);

スクリプトをコンパイルするために、ホストアプリケーションが読み込み関数(SQLEXREADFUNC)を実装する必要がある。
この関数はスクリプトとともにコンパイラに与えられ、コンパイラが文字を読む必要があるときに毎回呼び出される。
これは成功したときには1文字を返し、ソースの終端では0を返す必要がある。

もし、sq_compileが成功したなら、コンパイル済みスクリプトがSquirrelの関数としてスタックにpushされる。

注意: スクリプトを実行するために、sq_compile()によって生成された関数はsq_call()で呼び出す必要がある。

次の例はファイルを読み込む"read"関数である。

SQInteger file_lexfeedASCII(SQUserPointer file) {
    int ret;
    char c;
    if( ( ret = fread(&c, sizeof(c), 1, (FILE *)file ) > 0) )
        return c;
    return 0;
}
int compile_file(HSQUIRRELVM v, const char* filename) {
    FILE* f = fopen(filename, "rb");
    if (f) {
        sq_compile(v, file_lexfeedASCII, file, filename, 1);
        fclose(f);
        return 1;
    }
    return 0;
}

コンパイラが文法エラーで失敗したなら、コンパイラはコンパイラエラーハンドラを呼び出そうとする。
これは次のように宣言する必要がある。

typedef void (*SQCOMPILERERROR)(HSQUIRRELVM /*v*/, const SQChar* /*desc*/,
const SQChar* /*source*/, SQInteger /*line*/, SQInteger /*column*/);

そして、これは次のAPIによってセットすることができる。

void sq_setcompilererrorhandler(HSQUIRRELVM v, SQCOMPILERERROR f);

関数の呼び出し

Squirrel関数を呼び出すためには、
その関数をスタックにpushし、続けて引数をpushして、それから関数sq_callを呼び出す必要がある。
この関数は引数をpopし、もしsq_callの第3引数がtrueであるなら戻り値をpushする。

sq_pushroottable(v);
sq_pushstring(v, "foo", -1);
sq_get(v, -2);         // ルートテーブルから関数を取得
sq_pushroottable(v);   // 'this' (関数の環境オブジェクト)
sq_pushinteger(v, 1);
sq_pushfloat(v, 2.0);
sq_pushstring(v, "three", -1);
sq_call(v, 4, SQFalse);
sq_pop(v, 2);          // ルートテーブルと関数のpop

これは次のSquirrelコードと等価である。

foo(1, 2.0, "three");

もしSquirrelコード実行中に、実行時エラーが発生した(もしくは例外が投げられた)なら、sq_callは失敗する。

C関数の作成

ネイティブC関数は次のプロトタイプを持つ必要がある。

typedef SQInteger (*SQFUNCTION)(HSQUIRRELVM);

引数は呼び出すVMのハンドルであり、戻り値は次のルールに従う整数である。

説明
1関数が戻り値を返すとき
0関数が戻り値を返さないとき
SQ_ERROR実行時エラーが投げられたとき

Cの関数ポインタから、新規の呼び出し可能なSquirrel関数を取得するには、sq_newclosure()を呼び出し、Cの関数をそれに渡す必要がある。

その関数が呼ばれたとき、スタックベースはその関数の第1引数で、トップは最後の引数である。
値を返すためには、その関数内で値をpushして戻り値を1にする必要がある。

次の例は、引数の型をプリントし、引数の数を返す関数である。

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
}

関数を登録する例を次に示す。

SQInteger register_global_func(HSQUIRRELVM v, SQFUNCTION f, const char* fname) {
    sq_pushroottable(v);
    sq_pushstring(v, fname, -1);
    sq_newclosure(v, f, 0, 0); // 新規関数を作成
    sq_createslot(v, -3);
    sq_pop(v, 1); // ルートテーブルをpop
}

テーブルと配列の操作

新規テーブルはsq_newtableを呼び出すことで作成される。
この関数は新規テーブルをスタックにpushする。

void sq_newtable (HSQUIRRELVM v);

新規スロットを作成するには、次の関数を使用する。

SQRESULT sq_createslot(HSQUIRRELVM v, SQInteger idx);

テーブルの委譲の設定や取得をするには、次の関数を使用する。

SQRESULT sq_setdelegate(HSQUIRRELVM v, SQInteger idx);
SQRESULT sq_getdelegate(HSQUIRRELVM v, SQInteger idx);

新規配列はsq_newarrayを使用する。これはスタックに新規配列をpushする。
引数サイズが1以上なら、その要素をnullで初期化する。

void sq_newarray (HSQUIRRELVM v, SQInteger size);

配列の最後に値を追加するには、次の関数を使用する。

SQRESULT sq_arrayappend(HSQUIRRELVM v, SQInteger idx);

配列の最後から値を削除するには、次の関数を使用する。

SQRESULT sq_arraypop(HSQUIRRELVM v, SQInteger idx, SQInteger pushval);

配列をリサイズするには、次の関数を使用する。

SQRESULT sq_arrayresize(HSQUIRRELVM v, SQInteger idx, SQInteger newsize);

配列やテーブルのサイズを取得するには、次の関数を使用する。

SQInteger sq_getsize(HSQUIRRELVM v, SQInteger idx);

配列やテーブルに値をセットするには、次の関数を使用する。

SQRESULT sq_set(HSQUIRRELVM v, SQInteger idx);

配列やテーブルから値を取得するには、次の関数を使用する。

SQRESULT sq_get(HSQUIRRELVM v, SQInteger idx);

委譲なしで配列やテーブルから値のセットや取得を行うには、次の関数を使用する。

SQRESULT sq_rawget(HSQUIRRELVM v, SQInteger idx);
SQRESULT sq_rawset(HSQUIRRELVM v, SQInteger idx);

配列やテーブルの要素を繰り返すには、次の関数を使用する。

SQRESULT sq_next(HSQUIRRELVM v, SQInteger idx);

これは繰り返しを行う例である。

// ここで、テーブルか配列をpush
sq_pushnull(v)  // null反復子
while(SQ_SUCCEEDED(sq_next(v, -2))) {
    // この時点で-1が値で、-2がキーとなる
    sq_pop(v, 2); // 次の反復の前にキーと値をpopする
}
sq_pop(v, 1); // null反復子をpop

ユーザデータとユーザポインタ

Squirrelはホストアプリケーションで任意のデータチャンクをSquirrelの値として代入することができる。これはユーザデータ型によって可能となる。

SQUserPointer sq_newuserdata (HSQUIRRELVM v, SQUnsignedInteger size);

関数sq_newuserdataが呼び出されたとき、Squirrelは新規ユーザデータを指定されたサイズでメモリに割り当て、そのバッファをポインタとして返し、そのオブジェクトをスタックにpushする。
この時点で、アプリケーションはこのメモリチャンクに任意のことを行うことができる。他のすべての組み込み型と同様に、VMは自動的にこのメモリの解放を行う。
ユーザデータは関数呼び出しやテーブルスロットへの保存に用いることができる。

デフォルトではSquirrelはユーザデータを直接操作することができない。しかし、委譲を代入して、それをテーブルのように振る舞うにようにすることができる。
ユーザデータ削除時に、アプリケーションはユーザデータオブジェクト内のデータに対して何かを行いたいときがある。ユーザデータが削除される前に、VMによって呼び出されるコールバックを設定することは可能である。
これはAPIのsq_setreleasehookを呼び出すことでできる。

typedef SQInteger (*SQRELEASEHOOK)(SQUserPointer, SQInteger size);
void sq_setreleasehook(HSQUIRRELVM v, SQInteger idx, SQRELEASEHOOK hook);

別の種類のユーザデータとしてユーザポインタがある。この型は通常のユーザデータのようなメモリチャンクではなくただの'void*'である。
これには委譲を持たせることはできず、値渡しになる。そのため、ユーザポインタのpushはメモリ割り当てが発生しない。

void sq_pushuserpointer(HSQUIRRELVM v, SQUserPointer p);

レジストリテーブル

レジストリテーブルはVMとVMのすべてのスレッド間で共有できる隠しテーブルである。
このテーブルはCのAPIからのみアクセス可能であり、ネイティブCライブラリ実装のための補助構造である。
例えば、sqstdlib(Squirrel標準ライブラリ)はこれを設定の保存とオブジェクト委譲の共有のために用いている。
レジストリテーブルはAPIのsq_pushregistrytableによってアクセス可能である。

void sq_pushregistrytable(HSQUIRRELVM v);

C言語のAPIからSquirrelの値への強参照の維持

SquirrelはCのAPIから値を参照することができる。関数sq_getstackobject()で、(任意の)Squirrelオブジェクトへのハンドルが取得できる。
オブジェクトハンドルは、オブジェクトへの参照の追加や削除によって、その生存期間を操作するために用いることができる。
オブジェクトはsq_pushobject()を用いることで、VMスタックに再pushすることができる。

HSQOBJECT obj;
sq_resetobject(v, &obj)          // ハンドルを初期化
sq_geststackobject(v, -2, &obj); // 位置-2からオブジェクトハンドルを得る
sq_addref(v, &obj);              // オブジェクトへの参照を追加
… //do stuff
sq_pushobject(v, &obj); // スタック上のオブジェクトをpush
sq_release(v, &obj);    // オブジェクトを解放

デバッグインタフェース

Squirrel VMは非常に単純なデバッグインタフェースを用意しており、これによって完全機能のデバッガを構築することが用意である。
sq_setdebughookによって、VMの1行ごとの実行時や関数の呼び出しや終了時に呼ばれるようなコールバック関数を設定することが可能である。このコールバックは現在の行、ソース、(あるなら)関数名を引数として持つ。

SQUIRREL_API void sq_setdebughook(HSQUIRRELVM v);

次のコードはデバッグフックがどのようなものかを示す(明らかにこの関数をC言語で実装することは可能である)。

function debughook(event_type, sourcefile, line, funcname) {
    local fname = funcname? funcname : "unknown";
    local srcfile = sourcefile? sourcefile : "unknown"
    switch (event_type) {
    case 'l': // called every line(that contains some code)
        ::print("LINE line [" + line + "] func [" + fname + "]");
        ::print("file [" + srcfile + "]\n");
      	 break;
    case 'c': // called when a function has been called
        ::print("LINE line [" + line + "] func [" + fname + "]");
        ::print("file [" + srcfile + "]\n");
        break;
    case 'r': // called when a function returns
        ::print("LINE line [" + line + "] func [" + fname + "]");
        ::print("file [" + srcfile + "]\n");
        break;
    }
}

引数event_typeは次のいずれかになる。

イベント型説明
l各行の実行時('l'ine)
c関数の呼び出し時('c'all)
r関数の終了時('r'eturn)

完全機能のデバッガは常にローカル変数と呼び出しスタックを表示することができる。
呼び出しスタック情報はsq_getstackinfos()によって検索することができる。

SQInteger sq_stackinfos(HSQUIRRELVM v, SQInteger level, SQStackInfos *si);

ローカル変数情報はsq_getlocal()によって検索することができる。

SQInteger sq_getlocal(HSQUIRRELVM v, SQUnsignedInteger level, SQUnsignedInteger nseq);

行ごとのコールバックを受けるには、スクリプトをデバッグ情報付きでコンパイルする必要がある。これはsq_enabledebuginfo()を使うことで可能になる。

void sq_enabledebuginfo(HSQUIRRELVM v, SQInteger debuginfo);