- blogs:
- cles::blog

RubyからWin32APIを叩く



バッチの動作を自動化しようとしたら、DLLを読み込んでAPIを叩く簡単なアプリを書かなければならない事が判明したのですが、僕はWin32で動くアプリ(C#, VC++, VB)を書いた事がないので困ってしまいました。
それだけのためにWin32アプリの描き方を覚えるのもアレなので、いつもバッチ処理で使っているRubyを使ってなんとか済ませられないかと調べてみると「Win32API - Rubyリファレンスマニュアル」に"DLL dllname をロードし、API関数 proc のオブジェクトを生成"という部分を見つけたのでこれを何とか使えないかといろいろいじって見たのでメモ。あまり使っている人がいないのか、資料が少なくて大変でした。
† 練習はUNZIP32.DLLで
とりあえず本番用のものを開発する前に適当なDLLで練習がしたかったので、DLLと聞いてすぐに思いついた統合アーカイバプロジェクトのUNZIP32.DLL for windows 9x/Me/NT/200x/XPをRubyから呼び出してみることにします。
† まず、C++のメソッドシグニチャの情報を探す
RubyのWin32APIのリファレンスを見る限りでは、C++で呼び出すときの関数のシグニチャの情報が必要になるみたいなので、開発用SDK版をダウンロードして適当な場所に展開しておきます。なるべく引数がシンプルなAPIをさがしてみたところ、UnZipGetVersion()は引数がvoidで戻り値がWORDというものがあったので、これを叩いてみることにします。
WINZIP32.API
WORD WINAPI UnZipGetVersion(VOID);
-----------------------------------------------------------------------
順序数 2
機能
UNZIP32.DLL の現在のバージョンを返します。
戻り値
現在のバージョン 75 -> Version 0.75
100 -> Version 1.00
540 -> Version 5.4
Windowsプログラムに疎いので、WORDが何をtypedefしたものなのか分からくてちょっと困りましたが、MSDNのWindows Data Typesによると「16-bit unsigned integer. The range is 0 through 65535 decimal.」書かれていました。RubyのWin32APIにはサイズの区別がないのですが、とりあえずintにマッピングできると考えていいんでしょうかね。
そうなると下記の部分は
このような感じになるのでしょうか。
† 実際にAPIを叩いてみる
コマンドラインを起動し、UNZIP32.DLLのあるディレクトリにcdしてirbを起動して下記のように実行します。
ちゃんとDLLのバージョンの5.4.2に相当する542が取れました。こんな呼び出し方で問題なさそうです。
† zipファイルを解凍してみる
とりあえず簡単なAPIが実行できたので、もう少しシグニチャが複雑なものを呼び出してみます。
WINZIP32.API
int WINAPI UnZip(const HWND hWnd,LPCSTR szCmdLine,LPSTR szOutput, const DWORD dwSize);
-----------------------------------------------------------------------
順序数 1
機能
圧縮/解凍を行います。
引数
hWnd UNZIP32.DLL を呼び出すアプリのウィンドウ・ハンドル。UNZIP32.DLL は実行時にこのウィンドウに対して EnableWindow() を実行しウィンドウの動作を抑制します。ウィンドウが存在しないコンソールアプリの場合や,指定する必要のない場合は NULL を渡します。
szCmdLine UNZIP32.DLL に渡すコマンド文字列。
注意
16ビット版から32ビット版では第一引数が増えてるので注意!
szOutput UNZIP32.DLL が結果を返すためのバッファ。グローバルメモリー等の場合はロックされている必要があります。64Kバイト以上のサイズでも問題ありません。
dwSize バッファのサイズ。結果が指定サイズを越える場合は、このサイズに切り詰められます。結果がこのサイズより小さい場合は、最後に NULL 文字が付加されます。(最低1文字のみが保証される)バッファのサイズいっぱいの場合等、NULL 文字がどこにもない可能性がある点に留意のこと。
戻り値
正常終了の時 0。
エラーが発生した場合 0 以外の数(エラー値 >= 0x8000:後述)。また,解凍先に既にファイルがあるなどの理由で解凍をスキップした場合などはスキップされたファイルの数を返します。(ただし、標準ではスキップ数ではなく、常に0を返します。スキップ数を得るためには、-qd オプションを指定する必要があります。UNZIP32D.TXT を参照)
DWORDはint、それ以外のハンドルとか文字列は全部ポインタとするしかないようです。
UNZIP32D.TXT
1. コマンドラインの形式
int WINAPI UnZip(const HWND hWnd,LPCSTR szCmdLine, LPSTR szOutput,const DWORD dwSize);
での szCmdLine で指定するコマンドラインの形式は次の通りです。
(注:16ビット版 UNZIP.DLL に対して、第一パラメータが追加されてます)
[-<command>] [[-<options>...] <archive_file_name>[.ZIP] [<directory_name>\] [[@<list_name>|<filespec>]...]
hWndはウィンドウは必要ないのでnil。szCmdLineに渡すコマンド文字列は上記のとおりになっていて、必須なのはアーカイブ名だけなので、そのままの文字列を渡せばいいのですが、szOutputのような副作用がある引数はちょっと特殊な事をしないといけないようです。
Win32API - Rubyリファレンスマニュアル
RubyのWin32APIのサンプルは固定長のヌル文字連続を送っている*1ので、szOutputはこれと同じようにヌル文字の連続を生成し、dwSizeにそのサイズをセットすることにします。下記の例ではカレントディレクトリにvar.txtというテキストファイルを圧縮した、foo.zipというアーカイブを解凍しています。
ここまでで、きちんと呼び出し自体は完了していて、カレントディレクトリにはvar.txtが作成されています。ひとまずこんな感じでやっていけば良さそうというのは分かりました。ただ、メッセージが戻ってきているbufの中身は末尾にヌル文字がついたままになってしまっていて、これを画面に表示したりするのはちょっとためらわれる感じ。
こういうバイナリを扱うのは確かpack()を使えば良かった気がするので、packテンプレート文字列 - Rubyリファレンスマニュアルを参考に一度unpackしてからもう一度packすることできちんと文字列として取り出す事ができました。これで大丈夫なのかはイマイチ自信が持てませんが。
ちなみにメッセージが長くなってバッファが足りないとどうなるのか気になったのでテスト。
途中で切れてしまうだけみたいです。
終端にヌル文字がつきませんが、Ruby内で扱う分にはこれは問題なさそうです。
† やっぱり1DLLで1つのオブジェクトにしたい
呼び出すAPIごとにオブジェクトができるのは違和感を覚えるので、DLLごとに1オブジェクトに纏めるとこんな感じでしょうか。
- *1: 考えたら、C言語は可変長の時はポインタと長さを送るのが普通でした。
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/3310
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。
OpenID を使ってログインすることができます。
2 . 福岡銀がデマの投稿者への刑事告訴を検討中(110981)
3 . 年次の人間ドックへ(110542)
4 . 2023 年分の確定申告完了!(1つめ)(110090)
5 . 三菱鉛筆がラミーを買収(109990)