Cデータの保存
組み込み言語では、ネイティブのデータを扱う必要がよくあります。Cのヒープで管理するメモリーへのポインターが必要な場合や、Wrenの動的な仕組みより効率的にデータのまとまりを保存したい場合があるでしょう。ファイルハンドルやデータベース接続などのネイティブリソースを表すWrenオブジェクトが必要な場合もあります。
そのような場合は、状態をWrenとCで半分ずつ持つ、混成の外部クラスを定義できます。名前、コンストラクター、メソッドを持つ、実際のWrenクラスです。Wrenで書いたメソッドや、Cで書いた外部メソッドを定義できます。渡したり、isで検査したりできる、実際のWrenオブジェクトを作ります。それと同時に、Wrenからは不透明で、Cからはアクセスできる、生のメモリーのまとまりを包みます。
外部クラスの定義 #
次のように定義します。
foreign class Point {
// ...
}
foreignキーワードは、クラスのインスタンスを構築するとき、ホストアプリケーションを関与させるようWrenへ伝えます。ホストは、外部インスタンスに何バイトの追加メモリーを持たせるかWrenへ伝え、Wrenは、そのデータを初期化する機会をホストへ与えます。
ホストアプリとやり取りするため、Wrenは外部クラスのインスタンスを構築するときに呼び出すC関数を必要とします。この関数は、外部メソッドの束縛と似た処理で探します。VMを設定するとき、WrenConfigurationのbindForeignClassFnフィールドを、定義したCのコールバックへ向けます。そのシグネチャは、次のとおりでなければなりません。
WrenForeignClassMethods bindForeignClass(
WrenVM* vm, const char* module, const char* className);
Wrenは、外部クラスの宣言を実行するとき、このコールバックを一度だけ呼び出します。外部クラスを含むモジュール名と、宣言するクラス名を渡します。ホストは、次の構造体を返す責任があります。
typedef struct
{
WrenForeignMethodFn allocate;
WrenFinalizerFn finalize;
} WrenForeignClassMethods;
これは、二つの関数ポインターです。最初のallocateは、外部クラスのインスタンスを作るたびにWrenが呼び出します。(省略可能なfinalizeコールバックは、後で説明します。)割り当てコールバックのシグネチャは、外部メソッドと同じです。
void allocate(WrenVM* vm);
インスタンスの初期化 #
コンストラクターを呼び出して外部クラスのインスタンスを作ると、Wrenは、その外部クラスを束縛するときに渡したallocateコールバックを呼び出します。そのコールバックで主に行うことは、何バイトの生のメモリーが必要かWrenへ伝えることです。次を呼び出します。
void* wrenSetSlotNewForeign(WrenVM* vm,
int slot, int classSlot, size_t size);
ほかのスロット操作関数と同様に、スロット配列を読み書きします。ほかの外部メソッドでも使えるよう汎用性を持たせるため、いくつかの引数があります。
slot引数は、新しい外部オブジェクトを置く、宛先スロットです。外部クラスの割り当てコールバックで呼び出すときは、0にします。classSlot引数は、構築する外部クラスがあるスロットです。VMが外部クラスの割り当てコールバックを呼び出すとき、クラス自身はすでにスロット0にあるので、これにも0を渡します。最後の
size引数が、注目する部分です。外部インスタンスに保存させたい、追加の生データのバイト数を渡します。Cから操作できるのが、このメモリーです。
例えば、8バイトのCデータを持つ外部インスタンスを作るには、次のように呼び出します。
void* data = wrenSetSlotNewForeign(vm, 0, 0, 8);
wrenSetSlotNewForeign()の戻り値は、要求したバイト領域への生のポインターです。(要求したバイト数に収まる限り)適切なCの型へキャストし、必要に応じて初期化できます。
コンストラクターへ渡した引数も、スロット配列の後続スロットで使えます。そのため、Wrenからコンストラクターへ渡した値を使って、外部データを初期化できます。
割り当てコールバックが戻ると、Wrenで書いたクラスのコンストラクターを実行し、通常どおり処理を続けます。その後、Wren内では、通常のクラスのインスタンスに見えます。ただ、その内部には、外部メソッドからアクセスできる追加のバイト領域があります。
外部データへのアクセス #
外部クラスのインスタンスに保存したデータは、通常、ほかの外部メソッドから使います。そのメソッドは同じ外部クラスに定義することが多いですが、ほかのクラスに定義してもかまいません。Wrenは、どちらでも扱えます。
スロットに外部インスタンスを入れたら、次を呼び出して、保存した生のバイト領域へアクセスできます。
void* wrenGetSlotForeign(WrenVM* vm, int slot);
外部オブジェクトを含むスロットのインデックスを渡すと、そのオブジェクトが包む生のメモリーへのポインターを返します。いつもどおり、C APIは型や範囲を検査しません。そのスロット内のオブジェクトが、実際に外部クラスのインスタンスであり、アクセスする分のメモリーを持つことを確認するのは、利用者の責任です。
そのvoidポインターを使って、指す先のデータを自由に読み書きできます。データは利用者のもので、Wrenは、それを保持するだけです。
リソースの解放 #
外部インスタンスがメモリーを保持するだけで、その寿命をWrenのガベージコレクターへ任せてよければ、これで完了です。Wrenは、参照がある限り、そのバイト領域を保持します。インスタンスへ到達できなくなれば、いずれガベージコレクターが処理し、メモリーを解放します。
しかし、外部データは、寿命を明示的に管理する必要のあるリソースを参照することがよくあります。例えば、開いたファイルハンドルを包む外部オブジェクトでは、GCが外部インスタンスを解放するとき、そのハンドルが開いたまま残らないようにする必要があります。
もちろん、外部クラスにclose()などのメソッドを追加して、オブジェクトが管理するリソースを利用者が明示的に解放できるようにできますし、通常は、そうするべきです。しかし、利用者が忘れ、オブジェクトへ到達できなくなった場合も、リソースが漏れないようにしたいでしょう。
そのため、外部クラスを束縛するとき、ファイナライザー関数も指定できます。WrenForeignClassMethods構造体の、もう一方のコールバックです。指定すると、ガベージコレクターが外部クラスのインスタンスを解放する直前に、Wrenが呼び出します。オブジェクトのリソースを片付ける、最後の機会です。
これはガベージコレクションの途中で呼び出すため、VMへ自由にはアクセスできません。通常の外部メソッドのように、スロットなどを操作することはできません。GCの実行中にそうすると、Wrenが不整合な状態になるおそれがあります。
そのため、finalizeコールバックのシグネチャは、次だけです。
void finalize(void* data);
Wrenが渡すのは、外部関数のメモリーへのポインターだけです。ファイナライザー内で行うべき唯一の処理は、そのメモリーから参照する、外部リソースの解放です。
完全な例 #
内容が多いので、ファイナライザーといくつかのメソッドを持つ、外部クラスの完全な例を見てみましょう。Cの標準ファイルAPIを包むFileクラスを作ります。
Wrenで作るクラスは、次のとおりです。
foreign class File {
construct create(path) {}
foreign write(text)
foreign close()
}
パスを指定して新しいファイルを作れます。作成後は書き込むことができ、必要なら明示的に閉じられます。また、利用者が閉じ忘れ、GCがオブジェクトを片付ける場合も、ファイルが閉じるようにする必要があります。
VMの準備 #
ホスト側では、まずVMを準備します。
#include "wren.h"
int main(int argc, const char* argv[])
{
WrenConfiguration config;
wrenInitConfiguration(&config);
config.bindForeignClassFn = bindForeignClass;
config.bindForeignMethodFn = bindForeignMethod;
WrenVM* vm = wrenNewVM(&config);
wrenInterpret(vm, "my_module", "some code...");
return 0;
}
外部クラスの束縛 #
VMへ二つのコールバックを渡します。最初は、外部クラス自身を結び付けるためのものです。
WrenForeignClassMethods bindForeignClass(
WrenVM* vm, const char* module, const char* className)
{
WrenForeignClassMethods methods;
if (strcmp(className, "File") == 0)
{
methods.allocate = fileAllocate;
methods.finalize = fileFinalize;
}
else
{
// Unknown class.
methods.allocate = NULL;
methods.finalize = NULL;
}
return methods;
}
束縛コールバックをFileクラスに対して呼び出したとき、VMが呼び出すべきallocate関数とfinalize関数を返します。割り当て処理は、次のようになります。
#include <stdio.h>
#include "wren.h"
void fileAllocate(WrenVM* vm)
{
FILE** file = (FILE**)wrenSetSlotNewForeign(vm,
0, 0, sizeof(FILE*));
const char* path = wrenGetSlotString(vm, 1);
*file = fopen(path, "w");
}
まず、wrenSetSlotNewForeign()を呼び出し、インスタンスを作ります。Cでファイルハンドルを表すFILE*を保存するのに十分な追加バイト数を指定します。そのバイト領域へのポインターを受け取ります。ファイルハンドル自身がポインターなので、二段階の間接参照となり、FILE**になります。通常は、単一の*になります。
スロット配列からファイルパスも取得します。次に、Cに、そのパスで新しいファイルを作らせます。新しいファイルハンドル、つまりFILE*を受け取り、*fileを使って、外部インスタンスへ保存します。これで、開いたファイルハンドルを包む外部オブジェクトができました。
ファイナライザーは、外部インスタンスのデータを適切な型へキャストし直し、ファイルを閉じるだけです。
void fileFinalize(void* data)
{
closeFile((FILE**) data);
}
次の小さな補助関数を使います。
static void closeFile(FILE** file)
{
// Already closed.
if (*file == NULL) return;
fclose(*file);
*file = NULL;
}
この関数は、まだ閉じていなければファイルを閉じ、ファイルハンドルをnullにします。これで、閉じたファイルを使おうとしないようにします。
外部メソッドの束縛 #
外部クラスの部分は、これで完了です。次は、いくつかの外部メソッドを扱います。ホストは、次の関数へのポインターをWrenへ渡し、メソッドの探し方をVMへ伝えます。
WrenForeignMethodFn bindForeignMethod(WrenVM* vm, const char* module,
const char* className, bool isStatic, const char* signature)
{
if (strcmp(className, "File") == 0)
{
if (!isStatic && strcmp(signature, "write(_)") == 0)
{
return fileWrite;
}
if (!isStatic && strcmp(signature, "close()") == 0)
{
return fileClose;
}
}
// Unknown method.
return NULL;
}
Wrenがこれを呼び出すと、クラス名とメソッド名を調べ、束縛するメソッドを特定し、適した関数へのポインターを返します。ファイルへ書き込む外部メソッドは、次のとおりです。
void fileWrite(WrenVM* vm)
{
FILE** file = (FILE**)wrenGetSlotForeign(vm, 0);
// Make sure the file is still open.
if (*file == NULL)
{
wrenSetSlotString(vm, 0, "Cannot write to a closed file.");
wrenAbortFiber(vm, 0);
return;
}
const char* text = wrenGetSlotString(vm, 1);
fwrite(text, sizeof(char), strlen(text), *file);
}
wrenGetSlotForeign()を使い、スロット配列から外部データを取得します。このメソッドは、ファイル自身を対象に呼び出すので、外部オブジェクトはスロット0にあります。受け取ったポインターを、適切な型のポインターへキャストします。ここでも、外部データ自身がポインターなので、ポインターへのポインターを受け取ります。
利用者がすでに閉じたファイルへ書き込んでいないか、簡単に確認します。そうでなければ、fwrite()を呼び出してファイルへ書き込みます。
もう一つのメソッドはclose()で、利用者が明示的にファイルを閉じられるようにします。
void fileClose(WrenVM* vm)
{
FILE** file = (FILE**)wrenGetSlotForeign(vm, 0);
closeFile(file);
}
上で定義したのと同じ補助関数を使います。これで、ファイナライザーといくつかの外部メソッドを持つ、完全な外部クラスができました。Wrenでは、次のように使えます。
var file = File.create("some/path.txt")
file.write("some text")
file.close()
便利でしょう? このクラスは、通常のWrenクラスと同じように見え、同じように扱えますが、ネイティブCコードの機能と、その性能の多くを備えています。