Wrenの組み込み
Wrenは、ホストアプリケーションの中で動くスクリプト言語として設計されています。そのため、組み込みAPIは、言語の各機能と同じくらい重要です。このAPIを適切に設計するには、いくつかの制約を満たす必要があります。
Wrenは動的型付けですが、Cは違います。Wrenの変数には、どの型の値でも格納できますが、Cでは、何らかのバリアント型を定義しない限り、そうはいきません。そして、バリアント型を定義しても、結局は問題を先送りするだけです。最終的には、静的型付けのコードと動的型付けのコードの境界を越えて、データを移動する必要があります。
Wrenはガベージコレクションを使いますが、Cはメモリーを手動で管理します。GCによって、APIにいくつかの制約が加わります。VMは、まだ利用できるすべてのWrenオブジェクトを、ネイティブCコードから参照しているものも含めて、見つけられる必要があります。そうでなければ、まだ使用中のオブジェクトを解放してしまうかもしれません。
また、理想的には、Wrenが管理するメモリー領域への生のポインターを、ネイティブCコードに見せたくありません。多くのガベージコレクション方式では、メモリー内のオブジェクトを移動します。Cコードがオブジェクトを直接指せると、オブジェクトが移動した際、そのポインターは無効な場所を指すことになります。現在のWrenのGCはオブジェクトを移動しませんが、将来、その選択肢を残しておきたいのです。
組み込みAPIは高速でなければなりません。利用者は、APIを扱いやすくするため、その上に抽象化の層を加えるかもしれません。しかし、基本のAPIが、システムで得られる最大の性能を決めます。スタックの最下層なので、遅すぎる場合に、利用者がそれを回避して最適化する方法はありません。さらに低水準の代替手段もありません。
APIを使いやすいものにしたい。これは、最も柔らかい制約なので、最後に挙げています。もちろん、美しく使いやすいAPIにしたいと考えています。しかし、上の制約への対応は、本当に必要です。そのため、最初の三つの目標を達成するためには、少し手間のかかるものにすることも受け入れます。
幸い、この問題に取り組むのは私たちが初めてではありません。LuaのC APIに慣れていれば、Wrenも似ていると感じるでしょう。
性能と安全性 #
コードがVMの範囲に収まっているときは、かなり安全です。メソッド呼び出しを動的に検査し、捕捉して処理できる実行時エラーを発生させます。スタックがあふれそうになると、スタックを拡張します。一般に、Wrenコードの中では、クラッシュを避けるために、できる限りのことをします。
そもそも、高水準言語を使う理由は、Cより安全で生産的だからです。一方、Cは、利用者が何をしているか理解していることを前提とします。ポインターを不正にキャストしたり、ビットを誤って解釈したり、解放済みのメモリーを使ったりできます。その代わりに、非常に高い性能が得られます。Cが速い理由の多くは、制限装置や保護柵を外していることにあります。
Wrenの組み込みAPIは、この二つの世界の境界を定義し、Cの特徴の一部を引き継ぎます。組み込みAPIの各関数を呼び出すときは、正しく呼んでいることを前提とします。Cから、引数を三つ要求するWrenメソッドを呼び出すなら、三つの引数を渡したと信頼します。
デバッグビルドでは、可能な限り多くのことを調べるアサーションがありますが、リリースビルドでは、利用者が正しく操作することを前提とします。つまり、組み込みAPIを使う際は、ほかのCコードを書く場合と同じように注意が必要です。その代わり、かなり高速なAPIが得られます。
Wrenを組み入れる #
Wren VMを自分のプログラムへ組み入れる方法は、二つ(実際には三つ)あります。
静的ライブラリーまたは動的ライブラリーへリンクする。Wrenをビルドすると、リンクに使える共有ライブラリーと静的ライブラリーの両方を
libに生成します。アプリケーションにソースを直接組み入れる。プログラムへソースを直接組み入れるなら、ビルド手順を実行する必要はありません。
src/vm内のソースファイルを、プロジェクトへ追加するだけです。C99、C++98、またはそれ以降の規格で、問題なくコンパイルできるはずです。
どちらの場合も、Wrenの公開ヘッダーを見つけられるよう、インクルードパスにsrc/includeを追加します。
#include "wren.h"
WrenはC標準ライブラリーだけに依存するため、通常は、ほかのものへリンクする必要はありません。一部のプラットフォーム(少なくともBSDとLinux)では、math.hの数学関数の一部が、独立したlibmライブラリーで実装されているため、明示的にリンクする必要があります。
プログラムがC++で、CとしてコンパイルしたWrenライブラリーへリンクする場合、このヘッダーが、CとC++の呼び出し規約の違いを処理します。
#include "wren.hpp"
Wren VMの作成 #
コードを実行可能ファイルへ組み入れたら、仮想マシンを作る必要があります。そのためには、WrenConfigurationオブジェクトを作り、初期化します。
WrenConfiguration config;
wrenInitConfiguration(&config);
これで、すべての項目に妥当な既定値を持つ基本設定が得られます。設定できることは後で詳しく説明しますが、ここでは、テキストを表示できるよう、writeFnだけを追加します。
まず、WrenがSystem.print(またはSystem.write)から送ってくる出力を、何らかの方法で処理する関数が必要です。出力に改行を含まない点に注意してください。
void writeFn(WrenVM* vm, const char* text) {
printf("%s", text);
}
続いて、その関数を指すように設定を更新します。
WrenConfiguration config;
wrenInitConfiguration(&config);
config.writeFn = &writeFn;
これが準備できたら、VMを作成できます。
WrenVM* vm = wrenNewVM(&config);
これは、新しいVMのためのメモリーを確保し、初期化します。WrenのC実装にはグローバルな状態がなく、Wrenが使うすべてのデータは、WrenVMの中にまとめられています。複数のWren VMを互いに独立して、問題なく実行でき、別々のスレッドで並行して動かすこともできます。
wrenNewVM()は設定のコピーを自分で保存するため、呼び出した後は、値を設定したWrenConfiguration構造体を破棄できます。これで、コードの実行を待つ、動作中のVMができました!
Wrenコードの実行 #
Wrenソースコードの文字列は、次のように実行します。
WrenInterpretResult result = wrenInterpret(
vm,
"my_module",
"System.print(\"I am running in a VM!\")");
この文字列は、改行で区切った一つ以上の文です。Wrenは文字列をコピーするため、呼び出した後に解放できます。wrenInterpret()を呼び出すと、まずソースをバイトコードへコンパイルします。エラーが起きると、直ちにWREN_RESULT_COMPILE_ERRORを返します。
エラーがなければ、Wrenは新しいファイバーを起動し、その中でコードを実行します。そのコードは、さらに必要なファイバーを自由に作れます。すべてのファイバーが完了するか、一つがsuspendするまで、ファイバーの実行を続けます。
実行時エラーが起き、ほかのファイバーが処理しなければ、メインファイバーまで順に中止し、WREN_RESULT_RUNTIME_ERRORを返します。そうでなければ、最後のファイバーが正常に戻ったとき、WREN_RESULT_SUCCESSを返します。
wrenInterpret()へ渡したすべてのコードは、特別な「main」モジュールで実行します。そのため、一つの呼び出しで定義したトップレベルの名前へ、後の呼び出しからアクセスできます。REPLのセッションに似ています。
VMの終了 #
処理が終わり、VMを終了したい場合は、そのVMが確保したメモリーを解放する必要があります。次のように行います。
wrenFreeVM(vm);
呼び出した後は、当然、渡したWrenVM*を再び使うことはできません。もう終了しています。
この呼び出しの時点で、まだ有効なWrenHandleオブジェクトがあると、Wrenは警告する点に注意してください。これにより、追跡できなくなったハンドルによるメモリーリークがないことと、VM解放後にハンドルを使おうとしないことを確認できます。
全体の例 #
以下は、上の内容をまとめた全体の例です。このファイルは、exampleフォルダーにあります。
//For more details, visit https://wren.io/embedding/
#include <stdio.h>
#include "wren.h"
static void writeFn(WrenVM* vm, const char* text)
{
printf("%s", text);
}
void errorFn(WrenVM* vm, WrenErrorType errorType,
const char* module, const int line,
const char* msg)
{
switch (errorType)
{
case WREN_ERROR_COMPILE:
{
printf("[%s line %d] [Error] %s\n", module, line, msg);
} break;
case WREN_ERROR_STACK_TRACE:
{
printf("[%s line %d] in %s\n", module, line, msg);
} break;
case WREN_ERROR_RUNTIME:
{
printf("[Runtime Error] %s\n", msg);
} break;
}
}
int main()
{
WrenConfiguration config;
wrenInitConfiguration(&config);
config.writeFn = &writeFn;
config.errorFn = &errorFn;
WrenVM* vm = wrenNewVM(&config);
const char* module = "main";
const char* script = "System.print(\"I am running in a VM!\")";
WrenInterpretResult result = wrenInterpret(vm, module, script);
switch (result) {
case WREN_RESULT_COMPILE_ERROR:
{ printf("Compile Error!\n"); } break;
case WREN_RESULT_RUNTIME_ERROR:
{ printf("Runtime Error!\n"); } break;
case WREN_RESULT_SUCCESS:
{ printf("Success!\n"); } break;
}
wrenFreeVM(vm);
}
次は、このVMで役立つ処理を行う方法を学びます。