SDL Source Tour Vol.5

とても便利なものなのですが、あまり有効に活用されていないような気がする SDL_RWopsの仕組について眺めてみます。 仕組といってもさほどのことはないですので、今回はちょっとアプローチを 変えてみたいと思います。つまり、自作のSDL_RWopsを作るための 基礎知識を得ることを目的としたいと考えています。

SDL_RWopsとは

RWopsのRはread、Wはwrite、opsはおそらくOperationsの略だと思います。 シーケンシャルにデータを読み書きする操作を抽象化したものです。 具体的には以下の機能をサポートしたものとなります

基本的に、SDL_RWのところをfに置き換えた関数(fread/fwrite/ftell/fseek/fclose)と 同じようなインターフェースとなっています。

SDL内で提供されている実体、というかつまり、この仕組の中で動く媒体には、

があります。

原理

SDL_RWops構造体は、include/SDL_rwops.hで 定義されています。

typedef struct SDL_RWops {
	int (*seek)(struct SDL_RWops *context, int offset, int whence);
	int (*read)(struct SDL_RWops *context, void *ptr, int size, int maxnum);
	int (*write)(struct SDL_RWops *context, const void *ptr, int size, int num);
	int (*close)(struct SDL_RWops *context);

	Uint32 type;
	union {
	    struct {
		int autoclose;
	 	FILE *fp;
	    } stdio;
	    struct {
		Uint8 *base;
	 	Uint8 *here;
		Uint8 *stop;
	    } mem;
	    struct {
		void *data1;
	    } unknown;
	} hidden;

} SDL_RWops;
コメントは省きました。まず、SDL_RWops* を第一引数にとる4つの 関数へのポインタ、seek、read、write、closeがあります。 これが、先程書いた処理に対応するわけです。tellはどうなった!! という 疑問は置いておきます。

そしてUint32のtypeというものがあります。これはまだ正体不明ですね。 そしてunionとしてstdio/mem/unknownというのがあります。 SDL_RWopsを使ったことがある方なら想像がつくでしょうが...

さらにSDL_rwops.hを眺めてゆくと以下のような行がみつかります。

/* Macros to easily read and write from an SDL_RWops structure */
#define SDL_RWseek(ctx, offset, whence)	(ctx)->seek(ctx, offset, whence)
#define SDL_RWtell(ctx)			(ctx)->seek(ctx, 0, SEEK_CUR)
#define SDL_RWread(ctx, ptr, size, n)	(ctx)->read(ctx, ptr, size, n)
#define SDL_RWwrite(ctx, ptr, size, n)	(ctx)->write(ctx, ptr, size, n)
#define SDL_RWclose(ctx)		(ctx)->close(ctx)
実際にはこれらのマクロを使って実行することになります。 第一引数で与えられたSDL_RWops*を使って、中の4つの関数ポインタを 呼び出しているだけです。tellはseekを呼び出していることも分かりますね。 tellは現在のポインタの位置を返しているようです。

では、実際にこれらの関数がどのように実装されているのかを眺めてゆくことに しましょう。

SDL_RWopsの生成

SDL_RWopsを生成するには以下の3種類の方法があります。

SDL_RWFromFile

では実装を見てみましょう。まずはSDL_RWFromFileからです。 SDL_RWops関連の実装はsrc/file/SDL_rwops.cにまとめられています。

SDL_RWops *SDL_RWFromFile(const char *file, const char *mode)
{
	FILE *fp;
	SDL_RWops *rwops;

	rwops = NULL;

#ifdef macintosh
	{
		char *mpath = unix_to_mac(file);
		fp = fopen(mpath, mode);
		free(mpath);
	}
#else
	fp = fopen(file, mode);
#endif
	if ( fp == NULL ) {
		SDL_SetError("Couldn't open %s", file);
	} else {
#ifdef WIN32
		in_sdl = 1;
		rwops = SDL_RWFromFP(fp, 1);
		in_sdl = 0;
#else
		rwops = SDL_RWFromFP(fp, 1);
#endif
	}
	return(rwops);
}
まず、macintoshの場合はunix_to_macというものでファイル名を変換します。 この関数の詳細は省略しますが、ざっと見た感じでは といったことをしているようです。私にはそれが何を意味しているのかは 分かりません。

いずれにしても、fopenでファイルを開き、それをSDL_RWFromFPに 渡しているようです。第二引数は1固定みたいですね。 またWIN32のときは、SDL_RWFromFPを呼ぶ前にin_sdlを1にして、戻ってきたら 0にしているようです(threadの問題発生しないのかな)

SDL_RWFromFP

続いてSDL_RWFromFPです。

SDL_RWops *SDL_RWFromFP(FILE *fp, int autoclose)
{
	SDL_RWops *rwops;

#ifdef WIN32
	if ( ! in_sdl ) {
		SDL_SetError("You can't pass a FILE pointer to a DLL (??)");
		/*return(NULL);*/
	}
#endif
	rwops = SDL_AllocRW();
	if ( rwops != NULL ) {
		rwops->seek = stdio_seek;
		rwops->read = stdio_read;
		rwops->write = stdio_write;
		rwops->close = stdio_close;
		rwops->hidden.stdio.fp = fp;
		rwops->hidden.stdio.autoclose = autoclose;
	}
	return(rwops);
}
まず、WIN32で、かつin_sdlが1だった場合はエラーになります。 理由はSDL_SetErrorにある通りです。DLL(つまりSDL本体)の中で FILE*を作って、それを渡すのは駄目らしいです。

ここで、SDL_AllocRWを呼び出した後、各メンバの設定を 行います。seek/read/write/closeに対応するのは、 stdio_seek/stdio_read/stdio_write/stdio_closeだそうです。 hidden共用体は、stdio構造体を使うようです。ここに fpとautocloseというメンバがありますので、引数で指定されてきたものを 格納します。

SDL_RWFromMem

SDL_RWFromMemは以下のような 感じです。

SDL_RWops *SDL_RWFromMem(void *mem, int size)
{
	SDL_RWops *rwops;

	rwops = SDL_AllocRW();
	if ( rwops != NULL ) {
		rwops->seek = mem_seek;
		rwops->read = mem_read;
		rwops->write = mem_write;
		rwops->close = mem_close;
		rwops->hidden.mem.base = (Uint8 *)mem;
		rwops->hidden.mem.here = rwops->hidden.mem.base;
		rwops->hidden.mem.stop = rwops->hidden.mem.base+size;
	}
	return(rwops);
}
やっぱりSDL_AllocRWを呼び出した後、パラメータを セットしています。今度はmem_*といった名前の関数を使用し、 hidden共用体にはmem構造体を使うようです。base/here/stop、ですから、 いかにも開始アドレス、現在アドレス、終了アドレスを指していそうな 気がしますし、実際コードを見てもそんな感じです。

SDL_AllocRW

今まで挙げてきた関数が下請けに使っているのがこのSDL_AllocRWです。 書くまでもないと思いますが、

SDL_RWops *SDL_AllocRW(void)
{
	SDL_RWops *area;

	area = (SDL_RWops *)malloc(sizeof *area);
	if ( area == NULL ) {
		SDL_OutOfMemory();
	}
	return(area);
}

void SDL_FreeRW(SDL_RWops *area)
{
	free(area);
}
SDL_RWopsのためのメモリを確保します。 同様に、SDL_FreeRWはそれを解放します。

実装について(1)

stdio_*やmem_*の実装を示して解説するのは簡単ですが、 ほとんど自明なコードばかりですのでそれは省略します。といっても、 何も説明しないのもあれなので、ここでは自作のSDL_RWopsを実装してみることに しましょう。 あまりにトリビアルで気が引けますが、ファイルディスクリプタ(open(2)で 手に入るやつ)を使ったSDL_RWopsを実装してみます。

まずはSDL_RWopsを作成する関数です。名前はSDL_RWFromFDとしましょう。 形はこんな感じでしょうか:

SDL_RWops* SDL_RWFromFD(int fd)
まずは、SDL_AllocRWを呼び出してSDL_RWopsのメモリを 作ってもらいましょう。
  SDL_RWops* rwops;
  rwops = SDL_AllocRW();
うまく作成できたら、実際に関数を登録します。
  if(rwops != NULL){
    rwops->seek = fd_seek;
    rwops->read = fd_read;
    rwops->write = fd_write;
    rwops->close = fd_close;
hiddenはどうしましょうか...stdioやmemがやっていたように、 fdをどこかに保存しておかないとfd_seek/read/write/closeで ファイルディスクリプタをとりだすことができません。 そのために、unknownメンバと、その中にあるdata1というポインタ(void*)が 用意されています。mem/stdioだけ特別扱いというのはなんか納得がいきませんが 仕方ありません。

ところで、SDL_RWFromFPにはautocloseという引数がありました。 コードを追ってみた方は分かると思うんですが、 SDL_RWclose()を呼び出したときに(実際に呼ばれるのはstdio_closeですが)、 fclose(fp)を実行するかどうかを決めることができます。 ユーザ側でfopenしたファイルポインタを渡したとき、SDL_RWops使った後に SDL_RWclose()したらファイルポインタも閉じられてた! それじゃ困る!という 場合はautocloseを0にすればよいということになります。逆に、 SDL_RWFromFileみたいに、内部でファイルポインタを作って SDL_RWFromFPに渡している場合は、SDL_RWcloseのついでにファイルポインタも 閉じてもらえると便利です。

というわけで、autocloseはユーザへの便宜(ファイルポインタを意識しなくても 使えるように)を図るためのものですので、SDL_RWFromFDにはautocloseを 用意しないことにします。

脱線してしまいましたが、そうするとhiddenで持ち運ばなければいけない データはfdだけとなります。unknown.data1はvoid*でfdはint... 本来であれば、unknown.data1にintを格納するだけのメモリを確保して、 そこにfdを入れるという処理を行うわけですが(そうすれば複数の パラメータも構造体へのポインタという形で渡せるわけです)、 横着してunknownに直接fdを入れてしまいます(邪悪ですので真似しないほうがいいと 思います)。

  rwops->hidden.unknown.data1 = (void*)fd;
そして最後にrwopsを返して終わりです。 全体のコードは以下のようになります。
SDL_RWops* SDL_RWFromFD(int fd)
{
  SDL_RWops* rwops;
  rwops = SDL_AllocRW();
  if(rwops != NULL){
    rwops->seek = fd_seek;
    rwops->read = fd_read;
    rwops->write = fd_write;
    rwops->close = fd_close;
    rwops->hidden.unknown.data1 = (void*)fd;
  }
  return rwops;
}
では実際にfd_*系の実装です。細かい規則を考えると正確では ありませんが、だいたい以下のような感じでも動くはずです。
int fd_read(SDL_RWops* context, void* ptr, int size, int maxnum)
{
  return read((int)context->hidden.unknown.data1, ptr, size * maxnum);
}
int fd_write(SDL_RWops* context, const void* ptr, int size, int num)
{
  return write((int)context->hidden.unknown.data1, ptr, size * num);
}

int fd_seek(SDL_RWops* context, int offset, int whence)
{
  return lseek((int)context->hidden.unknown.data1, offset, whence);
}

int fd_close(SDL_RWops* context)
{
  if(context){
    SDL_FreeRW(context);
  }
}

ここまでのまとめ

SDL_RWopsを使って入出力を書くメリットとしては、SDL_RWopsを作るときに 呼ぶ関数を変えるだけで出力先を簡単に変えることができるという点に尽きます。 また、自作のSDL_RWopsを実装すれば、すでにあるSDL内の関数の入出力を 自分好みに変更することも可能です。

例えば、SDL_SaveBMPという関数は、実際には以下のようなマクロで 定義されています(include/SDL_video.hより)

extern DECLSPEC int SDLCALL SDL_SaveBMP_RW
		(SDL_Surface *surface, SDL_RWops *dst, int freedst);

#define SDL_SaveBMP(surface, file) \
		SDL_SaveBMP_RW(surface, SDL_RWFromFile(file, "wb"), 1)
関数本体は、SDL_RWopsを受け取って、そこに出力を行う SDL_SaveBMP_RWという関数であり、SDL_SaveBMPは、その手前で SDL_RWFromFileによってSDL_RWopsを作っておくという 処理を行っています。(autocloseはSDL_RWFromFile内部で1になっていることに注意)

実装について(2)

もうちょっと実用的(?)な例として、SDL_Surfaceの一部に SDL_RWwriteで書込まれたデータを出力するようなSDL_RWopsを作ってみます。 ちょっとソースが大きくなってしまったので mbtest.zipに固めておきました (要SDL_kanji)。 仕様は以下の通りです

SDL_RWops* SDL_RWmessageBox(SDL_Surface* dst, SDL_Rect* area, 
  Kanji_Font* font, int fontsize)
dstが、書込みを行うSurface、areaはその中で使ってもよい領域、 fontはSDL_kanjiのfont、fontsizeはフォントの縦幅を指定します。

あとはSDL_RWwriteで書込みを行うと、その内容がdst内の指定された矩形領域内に 表示されます。↑の例では折り返しや改行の処理をサボってたりで 難がありすぎですが、とりあえず同じコードでstdoutとSDL_Surfaceに 出力されている様子が分かると思います。

残念ながら、SDL_RWopsには読込専用/書込専用といったものは 用意できないみたいなのですが(typeフィールドがその役目を果たしそうな 気はしますが...)今回は書込み専用ということで、 SDL_RWreadやSDL_RWseekはエラーを返すようにしています。

coming soon

ほかにもいろいろな応用が考えられそうなSDL_RWopsですが、見てきた通り 仕組は大変単純です。 実際、jpeglibのjpeg_destination_mgrとか、 mingのSWFInput、などなど、何らかの変換操作を伴うライブラリで、出力や入力を ファイルやメモリに仮定したくない場合は大抵使われるテクニックとなっています。

いろんな応用例を紹介したいところですが、ソースツアーの主旨から逸脱している ような気もしますので、それはまた別の場所で。

次回はタイマーとスレッドの話の予定です。


戻る

Zinnia (zinnia@risky-safety.org)
このWebコンテンツ(ここから辿れるもの)に対する コメントのメールは許可なく公開することがあります。 (最近多い無礼なメール対策であって穏当なメールをいきなり公開したり することはありません)