このページの内容

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コードの機能と、その性能の多くを備えています。

VMの設定 →← WrenからCを呼び出す