« C#でDirectXプログラミング | メイン | C# のための DirectX ライブラリ »

2006年04月14日

PInvokeとポインタの落とし穴 [C#]

DXライブラリには、GetHitKeyStateAll( char* *KeyStateBuf ) という関数がある。キーボードのすべてのキーの押下状態を取得して、char* 型のバッファに入れるというやつだ。これをC#から呼び出せなくて非常に困っている。

生粋の C++ ならこう書けばいい。

  char keys[256];             // キーの値を保存するバッファを確保
  GetHitKeyStateAll( keys );  // 全キーの状態を取得する。

  if( keys[ 0x1C ] == 1 ){    // 0x1C(Enterの定数)の部分が 1 ならば
    // Enterキーが押されている
  }

きちんと Enter キーが押されているか識別できる。しかし実は変数 keys の中身を見てみると、次のような状態になっているのだ。

keys = { "\0", "\0", "\0", ..., "\0", 1, "\0", "\0", ... };
   // 0x1C 番目だけが 1 であとは "\0"(ナル文字)

任意のアドレスに自由にアクセスできる C++ なら、この中から 0x1C 番目の値を参照してもなんら問題無いのだ。

この GetHitKeysStateAll 関数を DxLibDll.dll を通して C# から使おうとすると、こうなる。

  // DLL から当該関数を持ってくる(実際は関数の外に記述)
  [DllImport("DxLibDll.dll")]
  extern static int dxGetHitKeyStateAll(StringBuilder KeyState);

	//

  StringBuilder keys = new StringBuilder( 256 ); // バッファを確保して
  dxGetHitKeyStateAll( keys );                     // 全キーの状態を取得する
  if( keys.ToString().ToCharArray().[ 0x1C ] ){  // 0x1C(Enterの定数)の部分がが 1 ならば
    // Enterキーが押されています(?)
  }

しかし、実際はこのコードでは Enter キーの状態は取得することとができない。IndexOutOfRangeException が出るのだ。keys の値を参照してみると、配列長が 0 になっている。どういうことだろうか。これを C++ 的に表現すると、次のようになっているに等しい。

keys = { "\0" };

C# の string(や StringBuilder)は最初のナル文字("\0")が見つかった時点で後ろの文字列は全て切り捨ててしまう。よって、上記のような GetHitKeyStateAll の戻り値は一番最初のインデックスの時点で文字列が終了したと判断されてしまうわけだ。ナル文字の後ろに必要な文字があるというのは本来あるまじき状態だが、C++ はそれが“できてしまう”ためにこういうコードが書かれるわけだ。なにしろメモリ管理が全てこちらの判断に任されているのだから。

しかし C# はメモリを不用意に触らせてくれないので、アクセスできないところには基本的に手が届かない。試しに StringBuilderLength プロパティを 256 まで拡大しても、広げた後ろの部分をご丁寧にきちんと "\0" で初期化してくれる。無論、今 Enter キーを押してという情報も一緒にだ。第一、文字配列だからといってアドレスが連続しているという保証はどこにも無く、全く違う番地に拡張されることだって充分考えられるのだ。

しかし C# にはきちんと逃げ道が用意されている。unsafe スイッチだ。コンパイル引数に /unsafe をつけると、懐かしのポインタが再び使えるようになるのだ。下は、unsafe なコードで書いた GetHitKeysStateAll の利用だ。

  // DLL から当該関数を持ってくる(実際は関数の外に記述)
  [DllImport("DxLibDll.dll")]
  extern unsafe static int dxGetHitKeyStateAll(out char[] KeyState);

  //

  char[] keys = new char[256];  // char 型の配列でバッファを確保
  dxGetHitKeyStateAll( keys );  // 全キーの状態を取得する
  if( keys[ 0x1C ] == 1 ){      // 0x1C(Enterの定数)の部分が 1 ならば
    // Enterキーが押されている(?)
  }

しかし残念なことに、これでは動かなかった。マーシャリングエラー(型変換エラー)により強制終了してしまう。.NET Framework と Win32 API ではそもそも型の管理方法が違うからだろうか。ヘルプを見ても LPSTR(char*) に対応する C# の型は、StringBuilder のみになっている。char*char[] では、やはり駄目なのだろう。

unsafe コードが駄目だとなると、一体どうすれば GetHitKeyStateAll の値を取り出せるのだろうか。考え付くのは、さらに C++ で GetHitKeyStateAll のラッパー作ることぐらいしかないのだが。どなたか良い方法がありましたら教えていただけませんか。

投稿者 : 22:30 | コメント (6) | トラックバック (0)

トラックバック

このエントリーのトラックバックURL:
http://totora.jpn.org/mt/mt-tb.cgi/124

コメント

byte型でcastとかで出来ないかな?

投稿者 DeaR : 2006年04月15日 19:28

C#のStringBuilder で拾った時点で、すでに後ろが切れちゃってるんですよね。なので、DxLibDll.dll自体を弄ってキャストしないと難しいのかも。
私が把握している限りはですが・・・

投稿者 Anonymous : 2006年04月16日 18:28

 まぁ、C#使ったこと無い人間ゆえ、サパーリわかりませぬorz
extern unsafe static int dxGetHitKeyStateAll(byte* KeyState);
 なんかでいけないかなぁ、と。いかにもC/C++人間な考えを出してみるテスト。

 C#を弄る暇さえ確保できれば色々検証するんだけどなぁ(=x=;

投稿者 DeaR : 2006年04月17日 13:06

東方に似たゲームがDXライブラリ使ったソース付きで公開されてるってことで探していたら偶然たどり着きました。既に2年経ってるので事情が違うかもしれませんが、普通に

byte[] keys = new byte[256];
DX.GetHitKeyStateAll(out keys[0]);
if (keys[0x1C] == 1)
{
  DX.DrawString(0, 0, "Enterキーが押されています", DX.GetColor(255, 255, 255));
}

で出来ました。
ポインタを添え字使ってアクセスするときはfixedでアドレスを固定してやる必要があるんですけど、その辺が見受けられないので当時はそこが問題だったのではないでしょうか。
あとunsafe{}で囲った方がいいですね。
refやoutは所詮リファレンスなので、unsafeは多分あまり関係ないと思います。
ではご参考までに。失礼します。

投稿者 tok : 2008年02月06日 07:56

My best friend told me about this site. I thrust him, so I opened my Internet-browser and went surfing! After reading these news I was totally impressed by this article, even if it’s not true it’s written in a way that makes you believe it.

投稿者 Melanie : 2008年04月03日 17:46

Unknown message

投稿者 buyviagrache fЁ : 2010年12月25日 04:41

コメントしてください




保存しますか?

(書式を変更するような一部のHTMLタグを使うことができます)