BLOGTIMES
2009/11/27

RubyからWin32APIを叩く

  ruby  windows  cpp 
このエントリーをはてなブックマークに追加

バッチの動作を自動化しようとしたら、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にマッピングできると考えていいんでしょうかね。

そうなると下記の部分は

Win32API.new(dllname, proc, import, export)

このような感じになるのでしょうか。

Win32API.new('UNZIP32.DLL', 'UnZipGetVersion', %w(v), 'l')

実際にAPIを叩いてみる

コマンドラインを起動し、UNZIP32.DLLのあるディレクトリにcdしてirbを起動して下記のように実行します。

irb(main):001:0> require 'Win32API' => false irb(main):002:0> method = Win32API.new('UNZIP32.DLL', 'UnZipGetVersion', %w(v), 'l') => #<Win32API:0x2bfd950> irb(main):003:0> method.call() => 542 irb(main):004:0>

ちゃんと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、それ以外のハンドルとか文字列は全部ポインタとするしかないようです。

Win32API.new('UNZIP32.DLL', 'UnZip', %w(p p p l), 'i')

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リファレンスマニュアル

obj = Win32API.new obj = Win32API.new 'dllname.dll', 'foo', 'p', 'v' arg = "\0" * 256 obj.call(arg)

RubyのWin32APIのサンプルは固定長のヌル文字連続を送っている*1ので、szOutputはこれと同じようにヌル文字の連続を生成し、dwSizeにそのサイズをセットすることにします。下記の例ではカレントディレクトリにvar.txtというテキストファイルを圧縮した、foo.zipというアーカイブを解凍しています。

irb(main):001:0> require 'Win32API' => false irb(main):002:0> method = Win32API.new('UNZIP32.DLL', 'UnZip', %w(p p p l), 'i') => #<Win32API:0x2bfd860> irb(main):003:0> buf = "\0" * 128 => "\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000 \000\000\000\000\000\000\000\000\000" irb(main):004:0> cmd = "foo.zip" => "foo.zip" irb(main):005:0> method.call(nil, cmd, buf, buf.size) => 0 irb(main):006:0> buf => "Extracting from foo.zip\nvar.txt extracting to var.txt \n\000\000\000\000\ 000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ 000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ 000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\ 000\000\000\000\000\000\000"

ここまでで、きちんと呼び出し自体は完了していて、カレントディレクトリにはvar.txtが作成されています。ひとまずこんな感じでやっていけば良さそうというのは分かりました。ただ、メッセージが戻ってきているbufの中身は末尾にヌル文字がついたままになってしまっていて、これを画面に表示したりするのはちょっとためらわれる感じ。

こういうバイナリを扱うのは確かpack()を使えば良かった気がするので、packテンプレート文字列 - Rubyリファレンスマニュアルを参考に一度unpackしてからもう一度packすることできちんと文字列として取り出す事ができました。これで大丈夫なのかはイマイチ自信が持てませんが。

irb(main):007:0> buf.unpack('A*').pack('A*') => "Extracting from foo.zip\nvar.txt extracting to var.txt \n"

ちなみにメッセージが長くなってバッファが足りないとどうなるのか気になったのでテスト。

irb(main):008:0> buf = "\0" * 2 => "\000\000" irb(main):009:0> method.call(nil, cmd, buf, buf.size) => 0 irb(main):010:0> buf => "Ex"

途中で切れてしまうだけみたいです。
終端にヌル文字がつきませんが、Ruby内で扱う分にはこれは問題なさそうです。

やっぱり1DLLで1つのオブジェクトにしたい

呼び出すAPIごとにオブジェクトができるのは違和感を覚えるので、DLLごとに1オブジェクトに纏めるとこんな感じでしょうか。

#!/usr/bin/ruby require 'Win32API' class Win32Proxy def initialize(dll_name) @methods = {} @dll_name = dll_name end def method_missing(sym, *args, &block) m = @methods[sym] m.send('call', *args, &block) end def register_method(name, params, return_type) @methods[name.to_sym] = Win32API.new(@dll_name, name, params, return_type) end end class UnZip32 < Win32Proxy UNZIP32 = 'UNZIP32.DLL' def initialize() super(UNZIP32) register_method('UnZipGetSubVersion', %w(v), 'l') register_method('UnZip', %w(p p p l), 'i') end end unzip32 = UnZip32.new puts unzip32.UnZipGetSubVersion() buf = "\0" * 128 cmd = "foo.zip" unzip32.UnZip(nil, cmd, buf, buf.size) puts buf.unpack('A*').pack('A*')
  • *1: 考えたら、C言語は可変長の時はポインタと長さを送るのが普通でした。

トラックバックについて
Trackback URL:
お気軽にどうぞ。トラックバック前にポリシーをお読みください。[policy]
このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/3310
Trackbacks
このエントリにトラックバックはありません
Comments
愛のあるツッコミをお気軽にどうぞ。[policy]
古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
コメントはありません
Comments Form

コメントは承認後の表示となります。
OpenIDでログインすると、即時に公開されます。

OpenID を使ってログインすることができます。

Identity URL: Yahoo! JAPAN IDでログイン