カタバミさんのプログラミングノート

日曜プログラマーがプログラミング関係のメモを記録するブログです。

Ruby 2.0.0とWin32APIで構造体内部の固定長文字列(char型)を扱う

本文

有名事項なのかもしれませんが、Win32APIを使って固定長文字列を含む構造体を扱おうとした時に文字化け(?)して手間取ったのでメモです。

結論としてはunpack("A*")の返す文字列がASCII-8BITであることが原因だったので、 String::force_encoding('SJIS') (文字コードがShift-JISのとき) or String::force_encoding('SJIS').encode('UTF-8') (文字コードUTF-8のとき) で解決しました。

以下のサンプルコードはWin32 APIのmixerGetDevCapsA関数を呼び出して引数に使ったMIXERCAPSA構造体の固定長文字列を出力します。packおよびunpackのテンプレート文字列についてはリファレンスマニュアルを参考にして下さい。

require 'Win32API'

MAXPNAMELEN = 32
sizeof_MIXERCAPS = 48

#構造体を格納するバッファーを用意する
mixerCapsBuf = [0,0,0,'\0'*MAXPNAMELEN,0,0].pack("S!S!I!A#{MAXPNAMELEN}L!L!")
#=> "\x00\x00\x00\x00\x00\x00\x00\x00\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\\0\x00\x00\x00\x00\x00\x00\x00\x00"
Win32API::new('winmm', 'mixerGetDevCapsA', 'IPI', 'I').call(0, mixerCapsBuf, sizeof_MIXERCAPS)
#=> 0
#取得した構造体の中身を出力する
buf = mixerCapsBuf.unpack("S!S!I!A#{MAXPNAMELEN}L!L!")
#=> [1, 104, 1537, "\x83X\x83s\x81[\x83J\x81[ (Realtek High Defini", 0, 1]
buf[3].force_encoding('SJIS')
#=> "スピーカー (Realtek High Defini"

最後のbuf[3].force_encoding('SJIS')文字コードSJISコマンドプロンプトでは正常に表示されますが、UTF-8が設定されていると化けます。その場合は最終行は以下のコードに書き換えてください。

buf[3].force_encoding('SJIS').encode('UTF-8')

本当は文字コードに関わらず適用できるコードを探すべきなのですが――

おまけ:構造体の利用で少し扱いやすく

余談ですが、構造体を使うとunpackしたデータそのものよりは扱いやすくなるかもしれません。

mixerCapsStruct = Struct::new('MixerCaps',
    :mixerID, :productID, :driverVersion, :productName, :supportBits, :destinations)
#=> Struct::MixerCaps
buf = mixerCapsBuf.unpack("S!S!I!A#{MAXPNAMELEN}L!L!")
#=> [1, 104, 1537, "\x83X\x83s\x81[\x83J\x81[ (Realtek High Defini", 0, 1]
mixerCaps = mixerCapsStruct.new(buf[0], buf[1], buf[2], buf[3].force_encoding('SJIS'), buf[4], buf[5])
#=> #<struct Struct::MixerCaps
# mixerID=1,
# productID=104,
# driverVersion=1537,
# productName="スピーカー (Realtek High Defini",
# supportBits=0,
# destinations=1>
mixerCaps.productName #or mixerCaps[:productName]
#=> "スピーカー (Realtek High Defini"

C形式の定義

MIXERCAPSA構造体とmixerGetDevCapsA関数のC形式の定義を示しておきます。出典はVS2013に付属するヘッダーファイルです。

#define MAXPNAMELEN      32     /* max product name length (including NULL) */
typedef struct tagMIXERCAPSA {
    WORD            wMid;                   /* manufacturer id */
    WORD            wPid;                   /* product id */
    MMVERSION       vDriverVersion;         /* version of the driver */
    CHAR            szPname[MAXPNAMELEN];   /* product name */
    DWORD           fdwSupport;             /* misc. support bits */
    DWORD           cDestinations;          /* count of destinations */
} MIXERCAPSA, *PMIXERCAPSA, *LPMIXERCAPSA;
WINMMAPI
MMRESULT
WINAPI
mixerGetDevCapsA(
    _In_ UINT_PTR uMxId,
    _Out_writes_bytes_(cbmxcaps) LPMIXERCAPSA pmxcaps,
    _In_ UINT cbmxcaps
    );