PowerShellでアンマネージドdllを使おうとしたらハマった話
タイトルのとおりです。まさかこんなにハマるとは思いませんでした。
以前からExcelで作られたVBAなんかをPowerShellで書き直していくことがあるとしたら、どんな感じになるかなーと思うことがありました。そういうときには必ずといっていいほどVC++などで書かれたdllを使ってiniファイルの読み込みだったりファイルの圧縮解凍だったりをしていたものですよね。そうなると、PowerShellからも同じようにそういったものを使う必要があるのではないかと。
そこで、ふと思い立って圧縮で有名な「Unlha32.dll」をPowerShellで使ってみようと思いました。おそらく、いまだにセキュリティに問題があるとしても使っている会社は多いのではないかと思いますし、VBAから自動化するためによく使用されていたのではないかと思ったからです。
なお、Unlha32.dllはすでに開発していらした方もアナウンスされているとおり、セキュリティで問題があるのでオススメはしません。ここでは単に試してみただけということでご理解ください。
一応、ブログ本文とGistの両方にサンプルコードを記載しています。
アンマネージドdllをPowerShellで使う方法
調べたところ、しっかり書いてあったのはTechNetの連載記事でした。3本立てになってます。
Use PowerShell to Interact with the Windows API: Part 1 - Hey, Scripting Guy! Blog http://blogs.technet.com/b/heyscriptingguy/archive/2013/06/25/use-powershell-to-interact-with-the-windows-api-part-1.aspx
Use PowerShell to Interact with the Windows API: Part 2 - Hey, Scripting Guy! Blog http://blogs.technet.com/b/heyscriptingguy/archive/2013/06/26/use-powershell-to-interact-with-windows-apis-part-2.aspx
Use PowerShell to Interact with the Windows API: Part 3 http://blogs.technet.com/b/heyscriptingguy/archive/2013/06/27/use-powershell-to-interact-with-the-windows-api-part-3.aspx
記事が3本立てになっているのは、方法が3つあるからのようです。今回はPart1で紹介されている単純そうな方法を選択しました。効率化しているツールを書き換えるのにソースが長くなるのはやめておこうということです。
Part1で紹介されている方法は「Add-Type」を使うものです。これが一番わかりやすそうでした。つぎにPart2では「.Netのメソッドとして実行する」方法です。ちょっと長くなっているのでやめました。最後にPart3では「System.Reflection」を使った方法が紹介されています。これまたえらいことになっています。
ということで、今回はPart1で採用されているAdd-Typeを使うことにしました。
結局のところ、この方法でも.Netの協力をいただかないとどうにもなりません。少しみやすいくらいです。
まずはC#やVB.netで行うように「DllImport」を使います。そしてWin32APIの関数を定義します。
$method = @' [DllImport("UNLHA32.DLL", CharSet = CharSet.Ansi)] public static extern int Unlha( IntPtr hwnd, // ウィンドウハンドル string szCmdLine, // コマンドライン System.Text.StringBuilder szOutput, // 処理結果文字列 int dwSize); // 引数szOutputの文字列サイズ '@
これをAdd-Typeコマンドレットで実行してやると使うことができるようになります。 Add-Typeでは「-PassThru」パラメータをつけて、出力でインスタンスを取得しています。なお、「-Language」パラメータでC#を指定していますが、Add-TypeはデフォルトがC#になっているので別に指定しなくても大丈夫です。
正直、アンマネージドdllを使うだけならここで終わりです。
まず最初にコンパイルエラーが出まくりで困りました。何が悪かったかというと、関数定義のところです。「StringBuilder」クラスでDLLの出力バッファを定義していますが、これが見つからない、参照が不足しているというものでした。usingを追加しようと思ったのですが、そうするとさらにダメでした。それは自明というやつなのですが、ここで定義されたものは「-NameSpace」パラメータおよび「-Name」で定義された名前空間、クラスの内部に埋め込まれるからです。だからusingなどはおかしなことになります。
ということで、フルで「System.Text.StringBuilder」として定義しています。
つぎにハマったのは定義したUnlha関数が使うことができるようになっていないという実行時エラーでした。そこで、あとから「extern」を追加しています。これまた「せやな」という感じです。
最後にうっかりだったのが、動作確認に使っているWindowsが64bitなのにdllは32bitということで以下のようなエラーになりました。これは実行時エラーです。
間違ったフォーマットのプログラムを読み込もうとしました。 (HRESULT からの例外:0x8007000B)
普段あまり意識しなくなっていましたが、Unlha32は名前のとおり32bit向けにコンパイルされたdllでした。だから64bitではそのままでは使うことができません。そこで、Add-Typeのパラメータである「-CompilerParameters」にプラットフォーム情報をオプションとして渡すことを考えました。しかし、結局これも同じ結果になります。なぜなら、いくらAdd-Typeしたコードがx86向けになったとしても、PowerShell本体が64bit版だからです。
そこで、64bit版のWindowsにはインストールされている32bit版用のPowerShellを使うことでうまくいきました。
C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell.exe C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell_ise.exe
ということで、全体を掲載しておきます。 ちょっと調べが足りないのですが、32bit版のPowerShell ISEで動作させると現在のフォルダとして「.」を使うと32bit版PowerShellの実行形式が置いてあるフォルダになりました。そこで明示的に「ここ」としてパスを指定しています。また、このサンプルでは拡張子が「ps1」のファイルだけが圧縮の対象にしてあります。
$method = @' [DllImport("UNLHA32.DLL", CharSet = CharSet.Ansi)] public static extern int Unlha( IntPtr hwnd, // ウィンドウハンドル string szCmdLine, // コマンドライン System.Text.StringBuilder szOutput, // 処理結果文字列 int dwSize); // 引数szOutputの文字列サイズ '@ $unlha = Add-Type -MemberDefinition $method -Name "Unlha32" -Namespace "Unlha" -PassThru -Language CSharp $dir = (pwd).Path $now = Get-Date -Format "yyyyMMddHHmmss" $archived = Join-Path $dir "archive-$now.lzh" $input = Join-Path $dir "*.ps1" $cmd = "a $archived $input" $output = [System.Text.StringBuilder](1024) $ret = 0 try{ $ret = $unlha::Unlha([System.IntPtr]0, $cmd, $output, $output.Capacity) if($ret -ne 0){ throw [System.ApplicationException]($output) } else { "Success ! $output" } } catch [Exception] { Write-Error ("エラーが発生`n" + $Error.Item($Error.Count - 1)) throw [System.ApplicationException] }
VBAで32bit版Windowsのころに作ってきたツール資産は、もしかするとかなりの負債になるかもしれません。こんなしょうもないレベルのものでもハマりどころが多いからです。もしも可能なら業務から見直しできると負債をどうにかしなくてもよくなるのはそうなのですが…難しいですね。