2012/03/29
KAICHO: s_naray[at]yahoo[dot]co[dot]jp
※SPAM防止のため捻ってある

吉里吉里プラグインの作り方

■はじめに

本文書は、吉里吉里を拡張する方法の一つ、吉里吉里プラグインの作り方について 述べる。

吉里吉里プラグインは、dll(共有ライブラリ)形式で提供される OSネイティブコードだ。簡単に言えば、TJSレベルでできることを増やす。 具体的には、既存のTJSクラスに自作関数やプロパティを追加したり、 新規の自作クラスを追加したりできる。詳細は ここいらへんに書いてあるが…正直これだけをベースに作れと言われても 何をすればいいのかさっぱり。で、実際探してみても殆ど情報がないので、 仕方ないから自分のためにもメモを残しておこうと思ったワケで。

本書は、C++が問題なく読んで組める人を対象にしている。そんなの判りませんな人は 完全においてけぼりなので注意。あと、我輩もそんな詳しいワケじゃないんで、 以下に間違いがあるかもしれない(というか絶対ある)。間違いを発見した 識者の方は「しょうがないなァのび太くんは!」とか言いながら 指摘していただけると、我輩が感涙に咽びます。多分。

■まずは環境を用意しよう

dllが作成できる環境があるなら、なんでもいいといえばいい。cygwin+gccとかでも いいし、gas使っても構わない。けど、やっぱり一般的なのがいいよな、という ことで、ここではMicrosoftが提供しているVisual C++ 2008 を使うことにする。 既に2010が出てるのになんで2008なのかというと、2010が吐くバイナリが Win2000上で動かないため。まぁWin2000はもうサポート終わったし、という意味では 乗り換えちゃってもいいんだけど、我輩はデバッグ用にVM上でWin2000を使うことが 多々あるので、そのためにもWin2000で動くのが必要になっているのですよ。 ただ、VC++2008はVC++2010に比較して最適化が極めて甘いという話もあって、 C++の最適化が必要な人はVC++2010の方がいいかもしんない。使い勝手はどちらも 殆ど変わらない。

で、VC++2008/2010なんぞは、 Express EditionならMicrosoftがタダで配っていたりする(ユーザ登録は必要)。 我輩のような貧乏人にはスバらしいお話なので、有難く使わせてもらおう。 リソースファイルの編集ができないとか ATLが入っていない とか色々あるが、 タダほど安いものはない、ということで、以下では VC++ 2008 Expressを前提に 話を進める。2010への読み替えは簡単だから特に注記しない。

■プロジェクト作成と必要なファイル群

VC++を導入したら、吉里吉里プラグインを作るために、プロジェクトを新規作成する。

  1. [ファイル]→[新規作成]→[プロジェクト]→[Win32コンソールアプリケーション]でプロジェクト作成
  2. ウィザードで[DLL(D)]、[空のプロジェクト]を選択して[完了]
  3. ソースファイルとヘッダーファイルに、 吉里吉里のソースコードから以下をインポート(コピーしてもいい)
    ファイル種別 ファイル名 吉里吉里ソースコード中の場所 備考
    ソースファイル ncbind.cpp kirikiri2/src/plugins/win32/ncbind/ -
    tp_stub.cpp kirikiri2/src/plugins/win32/ -
    ヘッダファイル layerExBase.hpp kirikiri2/src/plugins/win32/layerExRaster/ layerクラスを拡張する時のみ使用
    ncb_foreach.h kirikiri2/src/plugins/win32/ncbind/ -
    ncb_invoke.hpp kirikiri2/src/plugins/win32/ncbind/ -
    ncbind.hpp kirikiri2/src/plugins/win32/ncbind/ -
    tp_stub.h kirikiri2/src/plugins/win32/ -
  4. [プロジェクト]→[プロパティ]ダイアログで、[C/C++]→[コード生成]→[ランタイムライブラリ]→[マルチスレッド(/MT)]を選択
    → こうしないと、VC++をインストールしていない環境で、実行時に「MSVCR100.dllが存在しない」というエラーが出る

これでようやく吉里吉里プラグインのビルド環境が整った。あとはこれに、 自作プログラムのファイルを追加することで、VC++上で自作吉里吉里プラグインを ビルドできるようになる。

なお、ビルドしようとすると、以下の警告(Warning)が出力される場合がある。 これはただの警告かと思いきや、文字コードに関することなので、下手すると コンパイルが全然通らなくてエラーもりもり吐く、ということになる。

warning C4819 : ファイルは、現在のコード ページ (932) で表示できない文字を含んでいます。データの損失を防ぐために、ファイルを Unicode 形式で保存してください。

原因は、ファイルの文字コードがUnicodeに統一されていないため。VC++2008では デフォルトでUnicodeを使おうとするが、ここにCP932(アレだ、Shift-JISね)とかの ファイルが混ざってしまうと、コンパイラが困ってこういうエラーを吐く。 直し方は以下の三つのいずれか。

■事前に読んでおきたいもの

上でincludeしているncbind.hppはざっとでいいので読んでおいて欲しい。 概要は「電波とどいた?」ncbindのススヌに さらっと書いてある。…というかググル先生に聞いてもコレしか出てこないん ですけど。「このヘッダ使えば吉里吉里プラグイン作るのが超簡単!」…なんだけど、 この資料の無さだと多分パンピー置いてけぼり。「ヘッダ読めばわかるだろ」って 言われても、それは出来る人の論理であって、我輩みたく出来ない子は ついてけないであります。
じゃぁもうちょっと詳細はどこにあるんだよ!というと 吉里吉里ソースコードの中だったりして。 初心者に厳しい…。
本書を一通り読み終わったら、同じ場所にある testbind.cppを読むこともお勧めする。こちらはより実践的な例だ。

KAGEXもそうだけど、中身は本当にスバらしいのに、マニュアルがなくて (もしくはフツーには見あたらなくて)、あまり知られてないというのが残念。 我輩みたいに頭悪い人にはマニュアルってホント大事なんですよな。

■出来上がった吉里吉里プラグインの組み込み方

なんか 吉里吉里プラグインの説明に一番重要なことが書いてないので追記。 吉里吉里プラグインは、特に理由ない限り Override.tjs などで読み込み、 first.ksの先頭では読み込まないこと。first.ksの先頭で読み込むと、 それまでに作成されたインスタンスに影響を与えないから。…と言われて 「ああそうだね」と判らない人は、Override.tjsで読み込むようにしなさい必ず。
詳細は TJSをもっと使うためにの末尾の方、「初期化時に実行されるスクリプト」項を 参照。

ということで、ここから下は、実際に吉里吉里に関数やプロパティを追加する例を 述べていく。

■新たにTJSから使えるクラスを作る

これはもう ncbindのススヌを 読んでくだされ。それで一目瞭然っぽいし。

■既存のLayerクラスに関数を追加する(1)

まず、単純にグローバルな関数を Layer クラスに追加する吉里吉里プラグインを 書いてみよう。ここでは、二つの整数値を加算・減算して結果を返す add 関数と sub 関数を追加してみる。実際は非常に簡単だ。ncbind.hpp万歳。

#include "ncbind.hpp"

// 引数を加算して結果を返す関数
tjs_int myAdd(tjs_int a, tjs_int b)
{
	return a+b;
}

// 引数を減算して結果を返す関数
tjs_int mySub(tjs_int a, tjs_int b)
{
	return a-b;
}

// これらの関数をLayerクラスに登録
NCB_ATTACH_FUNCTION(add, Layer, myAdd);
NCB_ATTACH_FUNCTION(sub, Layer, mySub);

// 実際にTJS上から使う時は、例えば kag.fore.base.add(1, 2) のように呼び出す

NCB_ATTACH_FUNCTIONは、 「追加する名前」「追加先吉里吉里クラス」「追加する実際の関数」を指定すると、 指定した関数を指定した名前で指定クラスに追加してくれるマクロ。 これが用意されてるからこそ実に簡単。 ncbind.hppを読めばわかるけど、「追加する名前」は最終的に文字列に変換されて いるので、上のプログラム中で""で囲う必要はない。

■Layerクラスに関数を追加する(2)

ベタで関数を定義するだけでいいのであれば上のような定義でいいのだが、 例えば「自分で定義したクラスのメソッドを呼び出させたい」ことがある。 そんな時は、ちょっとだけ最後の定義が面倒になる。

#include "ncbind.hpp"

// 自分で定義したクラス
class MyClass {
public:
	// 引数を加算して結果を返すメソッド
	tjs_int add(tjs_int a, tjs_int b)
	{
		return a+b;
	}

	// 引数を減算して結果を返すメソッド
	tjs_int sub(tjs_int a, tjs_int b)
	{
		return a-b;
	}
};

// MyClassのメソッド add と sub を Layer クラスに追加する
NCB_ATTACH_CLASS(MyClass, Layer) {
	NCB_METHOD(add);
	NCB_METHOD(sub);
}

// 上と同じように、TJS上からは、例えば kag.fore.base.add(1, 2) のように呼び出す

最後の NCB_ATTACH_CLASS()は、「MyClass を Layer クラスに アタッチする」ことを示し、その { カッコ } 内で Layer クラスに追加する MyClass内のメソッドを指定している。まぁ…判ればどうということもないよね。

■Layerクラスにプロパティを追加する

プロパティは関数と違って引数を持たない。だから簡単、かなー、と思ったらそうでも なかったりする。NCB_PROPERTY() マクロを使うのだが、これがちょっと独特で、 getter と setter 関数を指定するようになっている。getterは値を得るための関数、 setterは値を設定するための関数だ。指定された整数値+1を覚えるという 頭が悪いプロパティ plusone をLayerクラスに追加してみよう。

#include "ncbind.hpp"

class MyClass {
	// 指定された値を覚える変数
	tjs_int num;
public:
	// 指定された値 + 1 を覚える
	void plusone_setter(tjs_int n)
	{
		num = n + 1;
	}

	// 覚えていた値を返す
	tjs_int plusone_getter()
	{
		return num;
	}
};

// プロパティ plusone を Layer クラスに追加する
NCB_ATTACH_CLASS(MyClass, Layer) {
	NCB_PROPERTY(plusone, plusone_getter, plusone_setter);
}

// TJS上からは、例えば kag.fore.layers[0].plusone = 3 のように使う

NCB_ATTACH_CLASS()を使うのは同じ。NCB_PROPERTYで、「登録する名前」「getter」 「setter」を引数に渡せばよい。なお、readonlyなプロパティなら NCB_PROPERTY_RO() なんてマクロも用意されていたりするので、例によってncbind.hppを見るよろし。

■吉里吉里プラグイン関数への引数

上ではなんとなく引数を処理してきたが、TJSで指定した引数が吉里吉里プラグインで 定義した関数へどのように渡るかを書いておきたい。TJS上では型という概念が薄いが、 吉里吉里プラグインに渡ってきた時点で型はイイカンジにキャストされる。…というか つまり吉里吉里プラグイン側でどう受け取るかによって適当に変換されるので あんま気にしなくていい。

TJS上での型吉里吉里プラグイン側での型備考
整数tjs_int64なんだけど、tjs_int(=int)で受け取ることが多い
浮動小数点doublefloatと書けばfloatで受け取れる
文字列tjs_char*通常はtcharつまりwchar_t
インスタンスtTJSVariant後述

大体指定したとおりに渡ってくるが、ただひとつ、インスタンスだけはtTJSVariant型で 渡ってくるので、その中のメンバ変数を取り出すためには、少々面倒な手順が必要。 例えば、Layerクラスのインスタンスが渡されてきた時に、そのメンバimageWidthを 引っ張り出すには、以下のような面倒な手順を踏まねばならない。

// 吉里吉里プラグインの中で定義した関数
// 引数の layer から、layer.imageWidth の値を取り出す例
void function(tTJSVariant layer)
{
	// まず tTJSDispatch2 型に変換して、
	iTJSDispatch2 *layerobj = layer.AsObjectNoAddRef();
	tTJSVariant var;
	// "imageWidth" メンバの値を取り出し、
	layerobj->PropGet(0, L"imageWidth", NULL, &var, layerobj);
	// それをキャストする
	tjs_int imageWidth = (tjs_int)var;

	(続きの処理)

一つならいいが、複数のメンバ変数の値を取り出そうとすると 面倒で死にそうになるので、 我輩は以下のようにgetTJSMember()という関数を定義して、これを使って メンバを引っ張りだすようにしている。ただ、本当はエラーチェックが必要で、 例えばメンバが存在しなかった時にどうするかとかを考えるべきで、 つまりtry節を設けるべき。とはいえ、 多くの場合はまぁ無視しちゃっていいかなー、と思ってたりしなくもないこの アンビバレンツっぷり。

// TJSインスタンスのメンバを得る
static tTJSVariant getTJSMember(iTJSValiant instance, const wchar_t param[])
{
	tTJSDispatch2 *obj = instance.AsObjectNoAddRef();
	tTJSVariant val;
	obj->PropGet(0, param, NULL, &val, obj);
	return val;
}

// 上と同じことがこれだけ短く書ける
void function(tTJSVariant layer)
{
	tjs_int imageWidth = (tjs_int)getTJSMember(layer, L"imageWidth");

	(続きの処理)

■吉里吉里プラグインから吉里吉里コンソール上にログを出力したい

C++の上から吉里吉里コンソールにログ出すことができるの?と問われれば、 うん、できる。 簡単に言えば、ncbind.hppの先頭で定義されているマクロを使えばよい。 文字列はtjs_char型(=wchar_t型)でなければならないことに注意。

NCB_WARN(L"表示する文字列");

あるいは、DEBUGラベルを定義するかどうかで動作が変わる NCB_LOG()を使ってもよい。 詳細は各自ncbind.hppの先頭を読むこと。

ただまぁ、コレだと文字列一つしか表示できなくて使い勝手が悪いので、 printf()と同じ形式で、1023文字までの文字列を表示する関数 log() を自前で 追加することをお勧めする。 モノ自体は結構簡単、これは吉里吉里のソースコードの各所にも書かれているので、 吉里吉里のソースコードを追いかけたことがあればすぐわかるだろう。

#include <stdio.h>
void log(const tjs_char *format, ...)
{
	va_list args;
	va_start(args, format);
	tjs_char msg[1024];
	_vsnwprintf_s(msg, 1024, format, args);
	TVPAddLog(msg);
	va_end(args);
}

■Layerを操作する吉里吉里プラグインを書く時は

layerExBase.hpp をincludeし、自作クラスをこの派生クラスにすることを お勧めする(必須ではないと思うんだけど、断言できない…スマン)。 このヘッダでは、 レイヤを操作するのに便利なマクロがいくつか定義されているからだ。 最も助かるのは、_buffer、_width、_height、_pitch が使えるようになること。 これらは、自インスタンスのプロパティ、 mainImageBufferForWrite、imageWidth、imageHeight、 mainImageBufferPitch をそれぞれC++上から参照できる値で返してくれる。 面倒な処理なしにこれらのプロパティの値が手に入るからとても便利。 詳細はlayerExBase.hppの中を見て欲しい。短いし。

ただし、_buffer/_width/_height/_pitchは、ローカル変数ではない ことに注意。普通は別に困らないが、 アセンブラ(asm{ })組む時に直接 _width なんかを参照しちゃダメなのだ。 「エラーにならないがおかしな動作になる」という実に困ったちゃんなことになり、 我輩かなりハマったのでした。アセンブラ側から使いたいなら、別にローカル 変数を定義し、代入して使うこと。

もう一つ注意点は、今までのNCB_ATTACH_CLASS()マクロが使えなくなること。 理由は以下の二つなんだけど、我輩ようわからんのでこれらの詳細理由は 聞かないで欲しい。知ってたら教えて欲しい。なんでこのくらいマクロにしといて くんないのかもよくわかんない。

  1. コンストラクタで引数を layerExBase クラスのコンストラクタに渡す必要があるため
  2. NCB_INSTANCE_GETTER()内でobj->reset()する必要があるため
だもんで、layerExBase.hpp を使う時は、以下の例のように、NCB_ATTACH_CLASS()を 展開したもの(HCB_GET_INSTANCE_HOOK()からNCB_ATTACH_CLASS_WITH_HOOK()まで)を 使う。もうここは「そういうもの」として覚えておいてよいと思う。 layerExDrawとかLayerExRasterとかも全部同じだったし。

というところで、layerExBase を 使いつつ、 自レイヤの_width、_height、_pitchをコンソールに表示する printProp() 関数は 以下のように定義する。

#include "ncbind.hpp"
#include "layerExBase.hpp"

// 上で書いたログ表示用関数
#include <stdio.h>
void log(const tjs_char *format, ...)
{
	va_list args;
	va_start(args, format);
	tjs_char msg[1024];
	_vsnwprintf_s(msg, 1024, format, args);
	TVPAddLog(msg);
	va_end(args);
}


// このクラスは layerExBase を継承すること
class MyClass : public layerExBase {
public:
	// コンストラクタでは layerExBase()を呼ぶこと
	MyClass(DispatchT obj) : layerExBase(obj)
	{
		// 他に処理があれば追加
	}

	// _width、_height、_pitchを表示する関数
	void printProp()
	{
		log(L"幅 = %d, 高さ = %d, ピッチ = %d\n", _width, _height, _pitch);
	}
};


// ここから layerExBase を使う時のほぼ定型
// 実際は、ncbind.hpp の #define NCB_ATTACH_CLASS() 行を展開したものを少し変えただけ
NCB_GET_INSTANCE_HOOK(MyClass)
{
	// インスタンスゲッタ
	NCB_INSTANCE_GETTER(objthis) {
		// objthis を iTJSDispatch2* 型の引数とする
		// ネイティブインスタンスポインタ取得る
		ClassT* obj = GetNativeInstance(objthis);
		if (!obj) {
			// 存在しない場合は生成
			obj = new ClassT(objthis);
			// objthisにobjをネイティブインスタンスとして登録
			SetNativeInstance(objthis, obj);
		}
		obj->reset(); // なにやら追加処理
		return obj;
	}
	// デストラクタ(実際のメソッドが呼ばれた後に呼ばれる)
	~NCB_GET_INSTANCE_HOOK_CLASS () {
	}
};

// フックつきアタッチ
NCB_ATTACH_CLASS_WITH_HOOK(MyClass, Layer) {
	NCB_METHOD(printProp);
}

// TJS上からは、例えば kag.fore.layers[1].printProp() のようにして使う

■Layer上の画像データに直接アクセスする

Layer(のインスタンス)のプロパティは、全ていわゆる 吉里吉里リファレンスのLayerクラスの各記述に従って格納されている。 これはC++から覗く時でも全く同じ。 画像のバイナリ生データは、Layerクラスの中の mainImageBufferで参照できる。書き換える時は mainImageBufferForWriteで参照しなければならないことに注意。

画像の生データは、現在は以下のように並んでいる。

これだけ判っておけば、例えば「画像を赤成分のみにする」なんて関数 toRed() をLayerクラスに追加するのは簡単だろう。

#include "ncbind.hpp"
#include "layerExBase.hpp"

class MyClass : public layerExBase {
public:
	// コンストラクタでは layerExBase()を呼ぶこと
	MyClass(DispatchT obj) : layerExBase(obj) {}

	// 画像を赤成分のみにする
	void toRed()
	{
		for (int y = 0; y < _height; y++) {
			BYTE *p = (BYTE*)_buffer + y*_pitch;
			for (int x = 0; x < _width; x++) {
				*p++ = 0;	// 青成分を0に
				*p++ = 0;	// 緑成分を0に
				p++;		// 赤成分はスキップ
				p++;		// α値はスキップ
			}
		}
	}
};

// ここからは layerExBase を使う時のほぼ定型
NCB_GET_INSTANCE_HOOK(MyClass)
{
	// インスタンスゲッタ
	NCB_INSTANCE_GETTER(objthis) {
		// objthis を iTJSDispatch2* 型の引数とする
		// ネイティブインスタンスポインタ取得る
		ClassT* obj = GetNativeInstance(objthis);
		if (!obj) {
			// 存在しない場合は生成
			obj = new ClassT(objthis);
			// objthisにobjをネイティブインスタンスとして登録
			SetNativeInstance(objthis, obj);
		}
		obj->reset(); // なにやら追加処理
		return obj;
	}
	// デストラクタ(実際のメソッドが呼ばれた後に呼ばれる)
	~NCB_GET_INSTANCE_HOOK_CLASS () {
	}
};

// フックつきアタッチ
NCB_ATTACH_CLASS_WITH_HOOK(MyClass, Layer) {
	NCB_METHOD(toRed);
}

これをうんと難しくしていくと、layerEx*.dll のようなものが書ける、という ことでひとつ。

■リソースファイルを書く

DLLにバージョン入れたり作成者の名前入れたりできるのをご存知だろうか。 ファイルのプロパティを表示すると出てくる「バージョン情報」ちうタブ。あそこに 表示される情報は、プロジェクトにリソースファイル(.rcファイル)を追加することで 適宜定義・変更することができる。

ところが、VC++ 2008 Express Edition では、「Expressなんだからそんなのつけなくて いいでしょ」という政治的な判断だかなんだか、リソースファイルを編集することが できない。キィッ!ユーザ舐めんなよ!なんだけど、実は他から持ってきて 追加することは可能だ。なので、以下にリソースファイルの例 (ある日のlayerExShimmerのrcファイル)を示すから、 これを適当に書き換えて使ってくだされ。プロジェクトへの追加は、 [リソースファイル]→[追加]→[既存の項目]からファイルを選択すればO.K.。

あんま中身の詳細は我輩も知らんのだがナ!(大問題)

#include <winver.h>

VS_VERSION_INFO    VERSIONINFO 
FILEVERSION        0,3,0,0
PRODUCTVERSION     0,3,0,0
FILEFLAGSMASK      VS_FFI_FILEFLAGSMASK 
FILEFLAGS          0x00000000L
FILEOS             VOS_NT_WINDOWS32
FILETYPE           VFT_DLL
FILESUBTYPE        VFT2_UNKNOWN
BEGIN
    BLOCK "VarFileInfo"
    BEGIN
        VALUE "Translation", 0x0409, 1200
    END

    BLOCK "StringFileInfo"
    BEGIN
        BLOCK "040904b0"
        BEGIN
            VALUE "CompanyName",       "KAICHO Soft"
            VALUE "FileDescription",   "layerExShimmer.dll, for 吉里吉里2"
            VALUE "FileVersion",       "0, 3, 0, 0\0"
//            VALUE "InternalName",    "layerExShimmer\0"
            VALUE "LegalCopyright",    "Copyright (C) 2011,2012 KAICHO\0"
            VALUE "OriginalFilename",  "layerExShimmer.dll\0"
//            VALUE "ProductName",     "layerExShimmer\0"
//            VALUE "ProductVersion",  "0, 3, 0, 0\0"
	    VALUE "SpecialBuild",      "2012/03/20\0"
        END 
    END
END


/*

Version up history

0.3.0.0		最外枠1の計算を、そこからはみ出ない範囲で実施するように修正
		マルチスレッド化
0.2.1.0		4dot未満の領域を扱えるよう修正
0.2.0.0		最初のリリース

*/

■おわりに

さぁ、知ってることは全部書いたよ!間違いは一杯あると思うけど、 情報がないのが一番マズいと思うので、恥ずかしげもなく公開しちゃうよ! こんなイイカゲンな情報が、それでも皆様の何かにお役に立てれば、 それは本当に僥倖なことであるよ。

そして識者の方!是非間違いを積極的に指摘してくださいお願いホントに。 既にもうわかんないことがいくつかあるんでスよ。