最終回 実際にC++/CXでWindowsランタイム・コンポーネントを作成する:連載:Windowsランタイム・コンポーネントによるコードの再利用(2/3 ページ)
mrubyというオープンソースの組み込み用プログラミング言語コアを組み込んだWindowsランタイム・コンポーネントをC++/CXを使って実際に作ってみよう。
APIの決定と実装
MrbFacadeは、mrubyをコンソール上で対話型に操作する「mirb」というプログラム*2をWindowsストア・アプリとして実装するためのファサードとして設計した*3。
*2 mirbのソース・ファイルは、mrubyの「mrbgems\mruby-bin-mirb\tools\mirb」以下に配置されている。
*3 このプログラムは、「WinMIrb」で配布している。
mirbの動作
mirbはユーザーが入力した文字列をmrubyにパースさせる。もしパースが完了すればmrubyに評価させて結果を表示し、完了していなければ次の入力と合成して再パースすることを繰り返す。これを実際に行っているのが以下の部分だ(mrib.cファイルのmain関数より抜粋)。
……略……
/* parse code */
parser = mrb_parser_new(mrb);
parser->s = ruby_code;
parser->send = ruby_code + strlen(ruby_code);
parser->lineno = 1;
mrb_parser_parse(parser, cxt);
code_block_open = is_code_block_open(parser);
if (code_block_open) {
/* no evaluation of code */
}
else {
if (0 < parser->nerr) {
/* syntax error */
printf("line %d: %s¥n", parser->error_buffer[0].lineno, parser->error_buffer[0].message);
}
else {
/* generate bytecode */
n = mrb_generate_code(mrb, parser);
/* evaluate the bytecode */
result = mrb_run(mrb,
/* pass a proc for evaulation */
mrb_proc_new(mrb, mrb->irep[n]),
mrb_top_self(mrb));
……以下略……
}
……略……
ユーザーが入力したスクリプトをmrubyにパース、評価させ、その結果を表示する。
ここで利用しているmrubyの関数を以下に示す。カッコ内はそれぞれの関数が定義されているファイルだ。
- mrb_parser_new: パーサーを作成する(src/parse.y)
- mrb_parser_parse: パースを実行する(src/parse.y)
- mrb_parser_free: パーサーを解放する(src/parse.y)
- mrb_generate_code: パース後の構文木からコードを生成する(src/codegen.c)
- mrb_run: 生成したコードを実行する(src/vm.c)
また、is_code_block_openはmirb.cファイルで実装されている、構文木が完成したかを判定する関数である。
APIの決定
ここで、プロキシ方式のWindowsランタイム・コンポーネントを作ったとすると、コンポーネントを利用する側のコードは以下のようになるだろう。なお、以下の疑似コードで、mrbはWindowsランタイム・コンポーネントのオブジェクト名とする。
var parser = mrb.ParserNew(); // パーサーの作成
parser.Source = ruby_code; // ソース(スクリプト片の設定)
parser.SourceEnd = ruby_code.length; // ソースの末尾を設定
parser.LineNo = 1; // 行番号の設定
parser.Parse(); // パースの実行
var code_block_open = parser.IsCodeBlockOpen(); // mirbのis_code_block_open関数を別途実装する
……以下略……
しかし、ファサードとして作成する場合であれば、上記の処理のうちスクリプト片をパースするところと、パースしたスクリプトをコンパイル、実行するところの2つのメソッドを提供すれば、ほとんどの場合に十分だということが分かる。
具体的には以下のようにコンポーネントの利用側が記述できるようにすればよい。
if (mrb.Parse(ruby_code)) { // ruby_codeはStringオブジェクト。Parseが真を返した場合、構文木が完成
if (mrb.Errors.Count > 0) { // 構文エラーがあれば、エラーを表示する
……
} else {
var result = mrb.Run(); // コンパイルし実行する
……
}
} else {
……
}
ここから、以下のようにクラスを定義できる。
public value struct MrbError sealed // エラー情報は値型とする
{
int LineNo;
Platform::String^ Message;
};
public ref class Mrb sealed // ファサード・クラス
{
public:
// 与えられた文字列をパースする。実行可能になれば真を返す
bool Parse(Platform::String^ code)
{
errors->Clear(); // エラー情報コレクションをクリアする
char* pc = ToMrbString(code); // Stringオブジェクトをchar*に変換する
parser = mrb_parser_new(mrb); // Runメソッドでも利用するのでフィールドに保存する
parser->s = pc;
parser->send = pc + strlen(pc);
parser->lineno = 1;
mrb_parser_parse(parser, context);
bool ret = is_code_block_open();
if (ret)
{
mrb_parser_free(parser); // エラーとなったのでパーサーを解放する
parser = NULL;
}
mrb_free(mrb, pc); // ToMrbString関数でアロケートしたメモリを解放する
return ret;
}
// パーサーが保持する構文木をコンパイルし実行する
Platform::String^ Run();
// エラー情報を保持するコレクションを返す
property Windows::Foundation::Collections::IVector<MrbError>^ Errors
{
Windows::Foundation::Collections::IVector<MrbError>^ get() { return errors; }
}
private:
// Windowsランタイム非互換の型を利用する関数、フィールドはprivateとしなければならない
// そのため、必要に応じて同一モジュール内のほかのクラスにはfriendアクセスを設定する
// String(Unicode)をchar*(UTF-8)に変換する
char* Mrb::ToMrbString(Platform::String^ str)
{
// msvcrt(Cランタイム)の文字列変換関数は利用できないのでWin32 APIを利用する
int cb = WideCharToMultiByte(CP_UTF8, 0,
str->Data(), str->Length(), NULL, 0, NULL, NULL);
char* buf = (char *)mrb_malloc(mrb, cb + 1);
WideCharToMultiByte(CP_UTF8, 0,
str->Data(), str->Length(), buf, cb, NULL, NULL);
buf[cb] = 0;
return buf;
}
mrbc_context* context;
mrb_parser_state* parser;
mrb_state* mrb;
bool is_code_block_open(); // mirb.cファイルからコピーした関数
Platform::Collections::Vector<MrbError>^ errors;
};
なお、実際のMrbFacadeのコードではParseメソッドの戻り値をmrubyオブジェクトのラッパーとするほか、コンポーネント利用側のオブジェクトをmrubyへ与えるための機構なども用意している。また、上のリストではWin32 APIを利用した文字列変換などを示すためにヘッダへ実装を記述している箇所があるが、実際のソースでは実装ファイル(.cppファイル)に記述してある。
上記のリストでは、mrubyのエラー情報をWindowsランタイムABIで利用する構造体として定義している。
C++/CXのWindowsランタイムABIで利用する構造体は「public value struct 構造体名 seald」として定義する。
また、構造体の全てのメンバはWindowsランタイム互換型のフィールドの必要がある。
一方、ファサードは、Windowsランタイム型のクラスなので「public ref class クラス名 sealed」として定義している。
利用側に提供するメソッドはpublicとし、パラメータと戻り値にはWindowsランタイム型を利用しなければならない。
ここでは、エラー情報をコレクションとして提供するために、Platform::Collections::Vector<T>型のオブジェクトを利用している。ただし、Platform::Collections::Vector<T>型のオブジェクトはWindowsランタイム互換型ではないため、そのままでは公開できない。そのため、フィールドはprivateに置き、publicには、Windows::Foundation::Collections::IVector<T>型のプロパティを用意している。IVector<T>は、インターフェイス(C++では抽象クラスに相当する)であり、Windowsランタイムにより呼び出し側の適切なオブジェクトにマップされる。.NET Framework言語であれば、System.Collections.Generic.IList<T>型として与えられる。
APIの実装
APIを決定したら後はソースを記述するだけである。ソースは通常のVisual C++と同様に記述すればよい。なお、Visual C++ 2012からはC++11の機能を利用できるようになった。Visual C++ 2010との差異については、MSDNサイトの「C++11 の機能 (Modern C++) 」を参照されたい。
全てのソース・コードについては、筆者が公開しているソース・コードを参照されたい。実装を行い、ビルドを実行すると、Windowsランタイム・コンポーネントが出来上がる。あとは、利用する側のプロジェクトからこれを参照するように設定し、実際に使用するだけだ。
Copyright© Digital Advantage Corp. All Rights Reserved.