SDL Source Tour Vol.2

はじめに

びっくりするほど時間が開いてしまいました。 その上これから書こうとしていることは前回予告とは微妙に違ってると 思いますので救いようがありません...が、まあ当分SDL 1.3が出る気配もないですから まあいいでしょう(殴

最近(これを書いているのは2002年11月です)、ジョイスティックまわりのことを いろいろ調べています。ので、そのついでにジョイスティックまわりの コードの理解を促すためにこのドキュメントを書くことにしました。

ジョイスティック概要

SDLdocに書いてあることの焼き直しですが、 SDLにおいてジョイスティックをいじるときは以下のような流れになります。

あるいは、イベントモデルを使わず、適宜 SDL_JoystickGet???系の関数で値を得ることもできます (そっちの方が使いやすいような気もします)。 このドキュメントでは、各々の段階において「そのとき何が起こっているのか」を 見てゆきたいと思います。が、その前にもう少し補足を。

SDLがジョイスティック関連のイベントとして送出するのは以下の4つです

Axis、Ball、Hat、Buttonがそれぞれいくつ備わっているのかは、 SDL_JoystickNum??? 関数を呼ぶことで得ることができます。なお、このときに 渡す「ハンドル」は、SDL_JoystickOpenで得られたSDL_Joystickという 構造体になります。ジョイスティックの状態を得たり、開いたジョイスティックを 閉じたりする際にはこのSDL_Joystickを使います。ただし、 SDL_JoystickNameは例外です。これだけは、SDL_JoystickOpenと同様に 番号によってジョイスティックを特定します。 なぜそうなっているのかは謎です。なお、SDL_JoystickNameで帰ってくるのは、 私の環境では/dev/uhid0といったジョイスティックのデバイス名でした。

今回は、イベントまわりの話はとりあえず置いておいて、 SDLがジョイスティックをどのように扱っているのかといったあたりを 眺めてみたいと思います。

初期化

ジョイスティック関連のソースはsrc/joystick/以下にあります。 構造としてはaudioのときとほとんど同じです。

では早速、SDL_Joystick.c を見てゆきましょう。 SDL_Init(SDL_INIT_JOYSTICK)つきで呼ばれたとき、実際にはこの ソースの中のSDL_JoystickInitという関数が呼ばれています(ひとつ上の ディレクトリのSDL.cを御覧ください)

int SDL_JoystickInit(void)
{
        int arraylen;
        int status;

        SDL_numjoysticks = 0;
        status = SDL_SYS_JoystickInit();
        if ( status >= 0 ) {
                arraylen = (status+1)*sizeof(*SDL_joysticks);
                SDL_joysticks = (SDL_Joystick **)malloc(arraylen);
                if ( SDL_joysticks == NULL ) {
                        SDL_numjoysticks = 0;
                } else {
                        memset(SDL_joysticks, 0, arraylen);
                }
                SDL_numjoysticks = status;
                status = 0;
        }
        default_joystick = NULL;
        return(status);
}
簡単に流れを読むと以下のような感じです: 以上の予測をもとに、初期化本体と思われるSDL_SYS_JoystickInitを 調べます。いちおうgrepしてみます...
zinnia@freesia:~/build/SDL/SDL-1.2.5/src/joystick[7]% grep SDL_SYS_JoystickInit **/*.c
SDL_joystick.c: status = SDL_SYS_JoystickInit();
amigaos/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
bsd/SDL_sysjoystick.c:SDL_SYS_JoystickInit(void)
darwin/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
dc/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
dummy/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
linux/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
macos/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
mint/SDL_sysjoystick.c:int SDL_SYS_JoystickInit(void)
win32/SDL_mmjoystick.c:int SDL_SYS_JoystickInit(void)
というわけで、ドライバごとにSDL_SYS_JoystickInitが存在します。 これを考えると、SDL_SYS_JoystickInitがドライバ依存の初期化を しているという予測は正しそうに見えます(いい加減白々しいですが)。 ではさっそくその本体を眺めてみましょう。 例によってbsd/SDL_sysjoystick.cを見てみます。
int
SDL_SYS_JoystickInit(void)
{
        char s[16];
        int i, fd;

        SDL_numjoysticks = 0;

        memset(joynames, NULL, sizeof(joynames));
        memset(joydevnames, NULL, sizeof(joydevnames));

        for (i = 0; i < MAX_UHID_JOYS; i++) {
                sprintf(s, "/dev/uhid%d", i);
                fd = open(s, O_RDWR);
                if (fd > 0) {
                        joynames[SDL_numjoysticks++] = strdup(s);
                        close(fd);
                }
        }
        for (i = 0; i < MAX_JOY_JOYS; i++) {
                sprintf(s, "/dev/joy%d", i);
                fd = open(s, O_RDWR);
                if (fd > 0) {
                        joynames[SDL_numjoysticks++] = strdup(s);
                        close(fd);
                }
        }

        /* Read the default USB HID usage table. */
        hid_init(NULL);

        return (SDL_numjoysticks);
}
まず、SDL_numjoysticksですが、これは SDL_Joystick.cにある変数です。 なんでそうしなければいけないのかは、ちょっと考えてみたんだけど 分かりませんでした。

実際のコードの概要を以下に書きます。

以上により、SDL_SYS_JoystickInitはドライバ依存の初期化を行い、 またジョイスティックの本数を調べてそれを返す処理を行っていることが 分かりました。

なお、MAX_UHID_JOYS、MAX_JOY_JOYSは、このソース冒頭で定義されています (configurableではありません)

SDL_NumJoysticks()、 あるいはSDL_JoystickName()が どんな処理をしているのか、 は、書くまでもないので省略します(実際には、SDL_JoystickNameは 下層のSDL_SYS_JoystickNameを呼び出しています)。 また、SDL_JoystickNameに限って SDL_Joystick構造体ではなくてジョイスティックの番号を指定させる理由、 それから帰ってくるジョイスティックの名前がデバイス名だった理由、は、 上のコードからなんとなく察しがつきそうですね。

ジョイスティックを開く

続いて、SDL_JoystickOpenの 処理を眺めてみます。100行前後の関数です。

SDL_JoystickOpenは、最終的には「ハンドル」であるSDL_Joystick構造体への ポインタを返します。つまり、SDL_Joystick構造体の中身をきちんとセットして 返す関数というつもりで読んでゆきます。

また同時に、SDL_JoystickInitでゼロクリアされたSDL_joysticks[] に 内容を保存してゆきます。すでにOpen済のジョイスティックを再度Openしたときは、 その内容を返します(113行目付近)。 SDL_joysticksは参照カウンタによって管理されており、 後に見るように、SDL_JoystickCloseで誰からも参照されなくなったら解放という 処理を行います。

その後、ドライバ依存のSDL_SYS_JoystickOpenを呼び出します。 成功したら、その結果に従って、SDL_Joystickの内容の残りを埋めてゆきます。

話が微妙に逸れますが、ここでSDL_Joystick構造体の中身を理解しておきましょう。 ユーザが目に触れる部分でこの定義を見てゆきますと、 SDL_joystick.h に 行きあたります。

struct _SDL_Joystick;
typedef struct _SDL_Joystick SDL_Joystick;
SDL_Joystickはstruct _SDL_Joystickの別名だそうです。 ではその定義はどこにあるのかといいますと、 SDL_sysjoystick.hにあります。

でも、このSDL_sysjoystick.h、名前の通り内部では使われていますが、 実際にmake installする際にはインストールの対象にはなっていません。 ↑に書いた2行だけが、SDL_Joystick構造体(と便宜的に呼びます。今後も同様)と その実体(同じく)である_SDL_Joystick構造体を結びつけることになります。

この状態で(つまりSDL_joystick.hだけが(SDL.h経由で)includeされた状態で) 実際にSDL_Joystickを使う場合はどんなことが起きるでしょうか。

つまりポインタは宣言できるし、その受渡しもできるけど、内部を参照したり、 ユーザ側で独自に確保したりということはできない(コンパイルエラーになる)という 状況が、この
struct _SDL_Joystick;
という中身なしの定義によって実現されています。 同様な手法はSDL内のいろんなところで使われています(たとえばSDL_Surfaceの 中にもありますし、すぐ下で見るように、SDL_Joystickの中でもやっぱりこの ようなポインタが存在します)。SDLでは、このようなポインタを作ることで、 ドライバ依存やユーザに見せるべきではない内部データを隠蔽するという デザインポリシーを持っているようです。

ついでなのでSDL_Joystick構造体が持ってる内容を見てみます。

/* The SDL joystick structure */
struct _SDL_Joystick {
	Uint8 index;		/* 番号 */
	const char *name;	/* 名前 - システム依存 */

	int naxes;		/* 軸の数 */
	Sint16 *axes;		/* 現在の軸の状態 */

	int nhats;		/* ハットの数 */
	Uint8 *hats;		/* 現在のハットの状態 */
	
	int nballs;		/* トラックボールの数 */
	struct balldelta {
		int dx;
		int dy;
	} *balls;		/* ボールの動き(差分) */
	
	int nbuttons;		/* ボタンの数 */
	Uint8 *buttons;		/* 現在のボタンの状態 */
	
	struct joystick_hwdata *hwdata;	/* ドライバ依存の情報 */

	int ref_count;		/* 参照カウンタ */
};
ref_countが先程見てきた参照カウンタになります。 struct joystick_hwdata *hwdataが、直前に述べた「内部データを 隠蔽するためのポインタ」です。実体は各ドライバ毎に定義され、 ドライバ内でのみ使用されています。

そのほか、 各要素(軸、ハット、トラックボール、ボタン)ごとにポインタが存在することが 分かります。ここでやっと SDL_JoystickOpenの話に 戻ってきます。122行目以降、 各要素の数に応じてそのポインタにメモリを確保します。 たとえば、ボタンが4つあった場合、 buttonsにはsizeof(Uint8) * 4のサイズのメモリが確保され、 ボタン0の状態はbuttons[0]といった風にアクセスすることができるわけです。

その後、各要素のメモリをゼロクリアし(155行目以降)、 SDL_joysticksの末尾に、ここで作成されたSDL_Joystick構造体(joystick)を 登録し、参照カウンタを1増します。ここで登録された内容に従って、 関数冒頭で「すでにOpenされていたらそれを返す」という処理が行われます。 最後に、joystickを戻り値として渡して終わりです。

内部状態の取得

SDL_JoystickOpenによってジョイスティックを開くことに成功すれば、 以後、SDL_JoystickGet??系の関数によってジョイスティックの現在の 状態を取得することができます。代表としてSDL_JoystickGetAxisを眺めてみましょう。

Sint16 SDL_JoystickGetAxis(SDL_Joystick *joystick, int axis)
{
	Sint16 state;

	if ( ! ValidJoystick(&joystick) ) {
		return(0);
	}
	if ( axis < joystick->naxes ) {
		state = joystick->axes[axis];
	} else {
		SDL_SetError("Joystick only has %d axes", joystick->naxes);
		state = 0;
	}
	return(state);
}
ValidJoystickは、名前の通り 与えられたjoystickが正当なものかどうかをチェックする関数です。 同じようなチェックを 至るところで行っていますので一つの関数にまとめたのでしょう。

共通のチェックが済んだら、次は指定された軸の番号が 本当に存在するかどうかをチェックします。軸の本数はnaxesに 保存されていますからそれよりも大きな値ならエラーとなります。 あとはjoystickの中にある軸の情報(axes)を 取り出すだけです。先程説明した通り、与えられた番号を配列の添字にする ことで情報を取り出すことができていますね。

Coming soon...

SDLでジョイスティックの取り扱いがどのようになっているのか、その 外周部分をひとまずさらってみました。

ここで疑問が生まれます。 SDL_JoystickGet??? 系の関数がSDL_Joystick構造体の中身を読んで 返しているだけなら、SDL_Joystick構造体に実際に軸やボールなどの 状態を格納するのはいったい誰なんでしょうか。 まあ値の読み出しかたはドライバ依存ですから、ドライバ側にそのコードが あることは想像がつきますが、ではいったいいつそのコードが呼ばれるんでしょうか。 呼び出しているのは誰なんでしょうか。

といったわけで、 こんなに早く出番が出てくるとは思いませんでしたが、次回はイベントモデルの 話になりそうです。


戻る

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