動的PInvokeによるアンマネージDLL関数の呼び出し
これまでは「Unlha32.dll」を使ってファイルを圧縮、展開する方法を紹介しましたが、他の統合アーカイバDLLを使った場合でも、ほぼ同じ方法でできます。基本的には、「Unlha32.dll」で使用した関数名の頭の「Unlha」の部分を、使用するDLLに合わせて変更すればよいだけです(これ以外にもコマンドの違いがあり、これが難しいのですが、今は脇においておきます)。例えば、「Unlha32.dll」ではUnlhaGetVersion関数を使ってDLLのバージョンを取得できますが、「Unzip32.dll」ではUnZipGetVersion関数、「Cab32.dll」ではCabGetVersion関数となり、使い方はまったく同じです。このように統合アーカイバDLLは統一された名前のAPIをもっていますので、統合アーカイバDLLを使えば、これらのDLLで扱うことのできる多種多様な形式の書庫に対応したアプリケーションを簡単に作成することができるのです。
それでは具体的にどのようにすればよいのでしょうか? 今まで紹介した方法を応用すると、DllImportAttribute属性を使って、対応させたいDLLの関数ごとにメソッドを次々と作っていけばよいということになります(通常はラッパークラスを作成することになるでしょう)。大抵の場合はこれで十分ですが、統合アーカイバDLLの利点を生かしているとは言えません。実行時にDLL名と関数名を指定してアンマネージDLL関数を呼び出すことができればスッキリしますが、残念ながらDllImportAttributeだけではできません。
しかし、幸いにして、.NET Framework 2.0で新たに追加されたMarshal.GetDelegateForFunctionPointerメソッドにより、それが可能になりました。このメソッドは、アンマネージ関数ポインタをマネージデリゲートに変換するメソッドです。つまり、Win32 APIのGetProcAddress関数を使って取得したアンマネージ関数ポインタをこのメソッドでデリゲートに変換することにより、マネージコードからアンマネージ関数を呼び出すことができるようになるのです。
コード
早速ですが、具体例を示しましょう。下の例では、「Unlha32.dll」のUnlha関数をこの方法で呼び出しています。必要最低限の、ごく簡単なコードです。
using System; using System.IO; using System.Text; using System.Runtime.InteropServices; using System.ComponentModel; class Program { //DLLモジュールをマップ [DllImport("kernel32", EntryPoint = "LoadLibrary", SetLastError = true, CharSet = CharSet.Auto, ExactSpelling = false)] private extern static IntPtr LoadLibrary( [MarshalAs(UnmanagedType.LPTStr)]string lpFileName); //マップを解除 [DllImport("kernel32", EntryPoint = "FreeLibrary", SetLastError = true, ExactSpelling = true)] private extern static bool FreeLibrary( IntPtr hModule); //関数のアドレスを取得 [DllImport("kernel32", EntryPoint = "GetProcAddress", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true)] private extern static IntPtr GetProcAddress( IntPtr hModule, [MarshalAs(UnmanagedType.LPStr)]string lpProcName); //書庫操作一般 [UnmanagedFunctionPointer( CallingConvention.Winapi, CharSet = CharSet.Ansi)] private delegate int ExecuteDelegate( IntPtr _hwnd, [MarshalAs(UnmanagedType.LPStr)]string _szCmdLine, [MarshalAs(UnmanagedType.LPStr)]StringBuilder _szOutput, uint _dwSize); static void Main(string[] args) { //DLLの名前 string dllName = "Unlha32.dll"; //実行する関数の名前 string funcName = "Unlha"; //展開するファイル string archiveFile = args[0]; //展開先のフォルダ string extractTo = System.Environment.GetFolderPath( Environment.SpecialFolder.DesktopDirectory); extractTo = Path.Combine(extractTo, Path.GetFileNameWithoutExtension(archiveFile)) + "\\"; //DLLをロード IntPtr hmod = LoadLibrary(dllName); if (hmod == IntPtr.Zero) { throw new Win32Exception(Marshal.GetLastWin32Error()); } try { //展開する IntPtr funcAddr = GetProcAddress(hmod, funcName); if (funcAddr == IntPtr.Zero) { throw new ApplicationException( funcName + "のアドレスを取得できませんでした。"); } ExecuteDelegate execute = (ExecuteDelegate)Marshal.GetDelegateForFunctionPointer( funcAddr, typeof(ExecuteDelegate)); int ret = execute(IntPtr.Zero, string.Format("x \"{0}\" \"{1}\" *", archiveFile, extractTo), null, 0); Console.WriteLine("'{0}'を'{1}'に展開しました。", archiveFile, extractTo); } catch { Console.WriteLine("'{0}の展開に失敗しました。", archiveFile); } finally { //開放する FreeLibrary(hmod); } Console.ReadLine(); } }
Imports System Imports System.IO Imports System.Text Imports System.Runtime.InteropServices Imports System.ComponentModel Class Program 'DLLモジュールをマップ <DllImport("kernel32", _ EntryPoint:="LoadLibrary", _ SetLastError:=True, _ CharSet:=CharSet.Auto, _ ExactSpelling:=False)> _ Private Shared Function LoadLibrary( _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpFileName As String _ ) As IntPtr End Function 'DLLモジュールの参照カウントを1つ減らす <DllImport("kernel32", _ EntryPoint:="FreeLibrary", _ SetLastError:=True, _ ExactSpelling:=True)> _ Private Shared Function FreeLibrary( _ ByVal hModule As IntPtr) As Boolean End Function '関数のアドレスを取得 <DllImport("kernel32", _ EntryPoint:="GetProcAddress", _ SetLastError:=True, _ CharSet:=CharSet.Ansi, _ ExactSpelling:=True)> _ Private Shared Function GetProcAddress( _ ByVal hModule As IntPtr, _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpProcName As String _ ) As IntPtr End Function '書庫操作一般 <UnmanagedFunctionPointer(CallingConvention.Winapi, _ CharSet:=CharSet.Ansi)> _ Private Delegate Function ExecuteDelegate( _ ByVal _hwnd As IntPtr, _ <MarshalAs(UnmanagedType.LPStr)> ByVal _szCmdLine As String, _ <MarshalAs(UnmanagedType.LPStr)> ByVal _szOutput As StringBuilder, _ ByVal _dwSize As UInteger) As Integer Shared Sub Main(ByVal args() As String) 'DLLの名前 Dim dllName As String = "Unlha32.dll" '実行する関数の名前 Dim funcName As String = "Unlha" '展開するファイル Dim archiveFile As String = args(0) '展開先のフォルダ Dim extractTo As String = System.Environment.GetFolderPath( _ Environment.SpecialFolder.DesktopDirectory) extractTo = Path.Combine(extractTo, _ Path.GetFileNameWithoutExtension(archiveFile)) + "\" 'DLLをロード Dim hmod As IntPtr = LoadLibrary(dllName) If hmod = IntPtr.Zero Then Throw New Win32Exception(Marshal.GetLastWin32Error()) End If Try '展開する Dim funcAddr As IntPtr = GetProcAddress(hmod, funcName) If funcAddr = IntPtr.Zero Then Throw New ApplicationException( _ funcName + "のアドレスを取得できませんでした。") End If Dim execute As ExecuteDelegate = _ CType(Marshal.GetDelegateForFunctionPointer( _ funcAddr, GetType(ExecuteDelegate)), ExecuteDelegate) Dim ret As Integer = execute( _ IntPtr.Zero, _ String.Format("x ""{0}"" ""{1}"" *", _ archiveFile, extractTo), _ Nothing, _ 0) Console.WriteLine("'{0}'を'{1}'に展開しました。", _ archiveFile, extractTo) Catch Console.WriteLine("'{0}の展開に失敗しました。", archiveFile) Finally '開放する FreeLibrary(hmod) End Try Console.ReadLine() End Sub End Class
まず、Unlha関数を呼び出すためのデリゲートを宣言する必要があります。上記の例では、「ExecuteDelegate」がそれです。DllImportAttribute属性を使った時に宣言したUnlhaメソッドと同じシグネチャですので、特に問題は無いでしょう。ただ、DllImportAttributeのフィールドで指定していた設定をどのように反映させるかという問題が残ります。
これを解決するのが、UnmanagedFunctionPointerAttribute属性です。DllImportAttributeのCallingConvention、SetLastError、CharSet、BestFitMapping、ThrowOnUnmappableCharフィールドで指定していた設定は、UnmanagedFunctionPointerAttribute属性を使うことで解決できます。上記のExecuteDelegateでは、UnmanagedFunctionPointerAttributeのCharSetフィールドをCharSet.Ansiとしています。EntryPointやExactSpellingは指定された名前の関数をどのように探すかを指定するものですので、ここでは必要ありません。
アンマネージDLLの関数ポインタは、Win32 APIのLoadLibraryとGetProcAddress関数で取得します。これをMarshal.GetDelegateForFunctionPointerメソッドでExecuteDelegateデリゲートに変換すれば、このデリゲートを通して「Unlha32.dll」のUnlha関数を呼び出すことができるようになります。最後には、Win32 APIのFreeLibrary関数を呼び出して、後始末をします。
サンプルの「CommonExtractor_Console」と「CommonCompressor_Console」プロジェクトは、前出の「LhaExtractor_Console」と「LhaCompressor_Console」プロジェクトをこの方法に書き換えたものです。DllImportAttribute属性を使った方法と比べかなり面倒になったように思われますが、それだけの価値があります。
「.NET Archiver」の作成
Marshal.GetDelegateForFunctionPointerメソッドを使った動的PInvokeの長所を発揮し、面白さを理解していただくために、「.NET Archiver」というファイルの圧縮、展開専用のアプリケーションを作ってみました。これは、統合アーカイバDLLの情報をXMLファイルに記述することにより、対応DLLを増やすことができるという機能を持ちます。つまり、アプリケーション自体を作り変えたり、統合アーカイバDLLのラッパークラスを作成するという必要がなく、DLL名や関数名などの情報を文字列で与えるだけで、そのDLLが扱える書庫に対応できるようになるのです。
しかし、今まで見てきたように、実際はそんなに簡単ではありません。「統合アーカイバDLL」といっても明確な仕様があるわけではありませんし、コマンドやさらに細かい点でも相違が多く、DLL名と関数名だけで解決されるものではありません。
今までの考察からすると、統合アーカイバDLLを使ったファイルの圧縮、展開でDLL別に必要な情報は、DLL名と関数名以外に次のようなものが挙げられるでしょう。
- ファイルの圧縮、展開で使用するコマンド
- フォルダを圧縮する時に使用する、すべてのファイルを表すワイルドカード(大抵は「*」で大丈夫と思われる)
- 圧縮時に使用する拡張子(拡張子を指定しないで圧縮すると自動的に適当な拡張子をつけてくれるDLLもあるが)
- レスポンスファイルを使うか
「.NET Archiver」では、これらの情報をXMLファイルで指定できるようになっています。「.NET Archiver」の初回実行時に実行ファイルのあるフォルダに作成される「archivers.config」がそれです。統合アーカイバDLLのほとんどの設定が既に書かれていますので、参考にしてください。いろいろ設定を変えて実験するのも面白いでしょう。
「.NET Archiver」の使い方に関しては、同梱されているREADMEをご覧ください。
指定したフォルダ以外の場所にファイルを展開してしまう問題について
書庫を展開するアーカイバのいくつかには、「Directory Traversal Vulnerability」(ディレクトリ走査の脆弱性)と呼ばれる問題が確認されています。これは、展開時に指定したフォルダ以外の場所にファイルが展開されてしまうという問題です。例えば、書庫内のファイルに「../」といった相対パスが付いていると、発生する恐れがあります。これを利用して、悪意のあるファイルが、ユーザーの予期せぬ場所に展開されてしまう可能性が指摘されています(詳しくは、『多くの解凍ソフトに指定外の場所へファイルが解凍されてしまう脆弱性が存在』をご覧ください)。
この問題は多くの統合アーカイバDLLでも指摘され、現在ではほとんどのDLLで対策が施されました。しかし、未だに対策のなされていないDLLもあります。このようなDLLでは、ダミーのDLLを使うという回避法があります。詳しくは、Noahのページなどをご覧ください。
統合アーカイバDLLを使うアプリケーション側でこの問題を回避するには、展開する前に展開するファイルのパスを調べ、別のフォルダに展開される恐れがあればユーザーに警告するという方法になりそうです。「.NET Archiver」では、残念ながらそこまでやっていません。
統合アーカイバDLLの相違について
この記事では各統合アーカイバDLL間の違いについて、幾つか指摘しました。その内最も厄介な相違は、コマンドとスイッチでしょう。
そこで、各統合アーカイバDLLのコマンドとスイッチを機能別に表にまとめ、「統合アーカイバDLLの相違」で公開することにしました。コマンドとスイッチ以外の違いもまとめてあります。これはWikiになっており、間違いや更新された箇所があれば、どなたでも修正できるようになっています。
まとめ
この記事では、動的PInvokeにより、統合アーカイバDLLを使って書庫を展開、作成する方法を紹介しました。まとめると、次のようになります。
- 統合アーカイバDLLのAPIをマネージコードから呼び出すには、PInvokeを用いる。
DllImportAttribute属性を使用したPInvokeでは、実行時にDLL名と関数名を指定できない。しかし、Marshal.GetDelegateForFunctionPointerメソッドを使えば、これが可能になる。- 「統合アーカイバDLL」といっても統一されていない部分が多い。コマンドやスイッチの違い以外に、すべてのファイルを表すワイルドカード、「-」や「@」ではじまるファイル名の処置、レスポンスファイルの使い方などの相違にも留意する必要がある。
参考資料
- 統合アーカイバプロジェクト
- アンマネージコードとの相互運用
- Noah
- DOBON.NET 『遅延バインディングによりアンマネージDLL関数を呼び出す』
