このページの内容

CからWrenを呼び出す

Cからは、wrenInterpret()を呼び出してWrenに処理を指示できますが、VMを動かす最適な方法とは限りません。まず、遅いという問題があります。渡したソースコードの文字列を解析し、コンパイルする必要があります。Wrenのコンパイラーはかなり速いものの、それでも相応の処理が必要です。

通信の方法としても効率的ではありません。引数をソースコード文字列内のリテラルへ変換するような扱いづらい方法を使わない限り、Wrenへ引数を渡せません。また、結果の値を受け取ることもできません。

wrenInterpret()は、VMへコードを読み込むのには適していますが、すでに読み込んだコードを実行する最適な方法ではありません。必要なのは、コンパイル済みのコードのまとまりを呼び出すことです。Wrenはオブジェクト指向なので、「コードのまとまり」は、関数ではなく、メソッドを意味します。

このためのC APIはwrenCall()です。CからWrenのメソッドを呼び出すには、いくつかのものが必要です。

  • 呼び出すメソッド。Wrenは動的型付けなので、名前で探します。さらに、引数の個数によるオーバーロードができるため、実際には完全なシグネチャが必要です。

  • メソッドを呼び出す対象のレシーバーオブジェクト。実際に呼び出すメソッドは、レシーバーのクラスが決めます。

  • メソッドへ渡す引数。

これらを一つずつ説明します。

メソッドハンドルの取得 #

次のようなWrenコードを実行すると、


object.someMethod(1, 2, 3)

実行時には、VMはobjectのクラスを調べ、シグネチャがsomeMethod(_,_,_)のメソッドを探す必要があります。メソッドを呼び出すたびに、少なくともシグネチャをハッシュ化するような文字列の操作を行っているように聞こえます。多くの動的言語は、そのように動作します。

しかし、想像できるとおり、それはかなり遅くなります。代わりに、Wrenは、その処理を可能な限りコンパイル時に行います。上のコードをバイトコードへコンパイルするとき、メソッドのシグネチャを、そのメソッドを一意に識別する数値であるメソッドシンボルへ変換します。この処理の中で、シグネチャを文字列として扱う必要があるのは、ここだけです。

実行時には、VMはレシーバーのクラスのメソッドテーブルで、メソッドのシンボルを探すだけです。実際、現在の実装では、シンボルは単なる、そのテーブルの配列インデックスです。これが、Wrenのメソッド呼び出しが高速な理由です。

Cからのメソッド呼び出しで、同じ速度の利点が得られなければ、残念です。これを実現するため、メソッド呼び出しの処理を二段階に分けます。まず、「コンパイル済み」のメソッドシグネチャを表すハンドルを作成します。


WrenHandle* wrenMakeCallHandle(WrenVM* vm, const char* signature);

これは、文字列のメソッドシグネチャを受け取り、コンパイル済みのメソッドシンボルを表す不透明なハンドルを返します。これで、レシーバーと引数を指定し、特定のメソッドを非常に速く呼び出すための、再利用可能なハンドルが得られました。

これは通常のWrenHandleなので、必要な限り保持できます。通常は、アプリケーションの性能が重要なループの外で一度だけ呼び出し、必要な間、再利用します。不要になったときにwrenReleaseHandle()を呼び出して解放するのは、利用者の責任です。

レシーバーの準備 #

メソッドは得られましたが、誰を対象に呼び出すのでしょうか? レシーバーが必要です。前の節を読めば予想できるように、スロットに保存してWrenへ渡します。特に、メソッド呼び出しのレシーバーは、スロット0へ入れます。

そのスロットに保存した、どんなオブジェクトでもレシーバーとして使えます。必要なら、数値を保存して、その数値の+を呼び出すこともできます。

CからWrenのコードを呼び出すためにレシーバーが必要なのは、奇妙に感じるかもしれません。Cは手続き型なので、Wrenの単独の関数を呼びたいと思うのは自然です。しかし、Wrenは手続き型ではありません。特定のオブジェクトへ論理的に結び付かない、実行可能な操作を定義したいなら、適したクラスに静的メソッドを定義するのが自然です。

例えば、ゲームエンジンを作っているとします。Cから、毎フレーム、すべてのエンティティーを更新するよう指示したいとします。エンティティーのリストはWren内で管理するため、C側には、update(_)を呼び出す明らかな対象オブジェクトがありません。そこで、静的メソッドにします。


class GameEngine {
  static update(elapsedTime) {
    // ...
  }
}

CからWrenのメソッドを呼び出すときは、静的メソッドを呼ぶことがよくあります。その場合も、レシーバーは必要です。ただし、レシーバーはクラス自身です。Wrenのクラスは第一級のオブジェクトであり、名前付きクラスを定義する際には、実際には、そのクラス名の変数を宣言し、クラスオブジェクトへの参照を保存しています。

そのクラスをトップレベルで宣言したとすれば、C APIには、それを探す方法があります。上のクラスのハンドルを、次のように取得できます。


// Load the class into slot 0.
wrenEnsureSlots(vm, 1);
wrenGetVariable(vm, "main", "GameEngine", 0);

update()を呼び出すたびに、これを行うこともできます。しかし、毎回「GameEngine」を名前で探すため、やはり少し遅くなります。速い方法は、クラスのハンドルを一度だけ作り、毎回使うことです。


// Load the class into slot 0.
wrenEnsureSlots(vm, 1);
wrenGetVariable(vm, "main", "GameEngine", 0);
WrenHandle* gameEngineClass = wrenGetSlotHandle(vm, 0);

これで、GameEngineのメソッドを呼び出すたび、その値をスロット0へ戻して保存します。


wrenSetSlotHandle(vm, 0, gameEngineClass);

性能が重要なループの外へwrenMakeCallHandle()を移したのと同じように、wrenGetVariable()の呼び出しも外へ移せます。もちろん、性能が重要でなければ、そうする必要はありません。

引数を渡す #

レシーバーをスロット0に準備できたので、次は、ほかの引数を渡します。GameEngineの例では、経過時間だけです。メソッドの引数は、レシーバーの後の連続したスロットへ入れます。したがって、経過時間はスロット1に入ります。準備には、どのスロット関数も使えます。この例では、次のようにするだけです。


wrenSetSlotDouble(vm, 1, elapsedTime);

メソッドの呼び出し #

必要なデータはすべて準備できたので、後は、VMにコードの実行開始を指示するだけです。


WrenInterpretResult wrenCall(WrenVM* vm, WrenHandle* method);

これは、wrenMakeCallHandle()で作成したメソッドハンドルを受け取ります。Wrenはコードの実行を開始し、レシーバーのメソッドを探して実行します。メソッドが戻るか、ファイバーがsuspendするまで、実行を続けます。

wrenCall()は、wrenInterpret()と同じWrenInterpretResult列挙値を返し、メソッドが正常に完了したか、実行時エラーが起きたかを知らせます。(wrenCall()は何もコンパイルしないので、WREN_ERROR_COMPILEを返すことはありません。)

戻り値の取得 #

wrenCall()が戻ると、スロット配列は残ります。スロット0にはメソッドの戻り値があり、どのスロット読み取り関数でもアクセスできます。戻り値が不要なら、無視できます。

CからWrenを動かす方法は、これで分かりました。それでは、Wrenへ制御を渡すには、どうするのでしょうか? 次の節で説明します。

WrenからCを呼び出す →← スロットとハンドル