SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

動的PInvokeによる統合アーカイバ仕様DLLを使用した書庫の展開と作成

統合アーカイバDLLのAPIをマネージコードから呼び出す


動的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関数を使って取得したアンマネージ関数ポインタをこのメソッドでデリゲートに変換することにより、マネージコードからアンマネージ関数を呼び出すことができるようになるのです。

補足
 .NET Framework 1.1以前で同様の事を行う方法に関しては、『遅延バインディングによりアンマネージDLL関数を呼び出す』を参考にしてください。

コード

 早速ですが、具体例を示しましょう。下の例では、「Unlha32.dll」のUnlha関数をこの方法で呼び出しています。必要最低限の、ごく簡単なコードです。

C#の場合
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();
    }
}
VB.NETの場合
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属性です。DllImportAttributeCallingConventionSetLastErrorCharSetBestFitMappingThrowOnUnmappableCharフィールドで指定していた設定は、UnmanagedFunctionPointerAttribute属性を使うことで解決できます。上記のExecuteDelegateでは、UnmanagedFunctionPointerAttributeCharSetフィールドCharSet.Ansiとしています。EntryPointExactSpellingは指定された名前の関数をどのように探すかを指定するものですので、ここでは必要ありません。

 アンマネージDLLの関数ポインタは、Win32 APIのLoadLibraryGetProcAddress関数で取得します。これを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」といっても統一されていない部分が多い。コマンドやスイッチの違い以外に、すべてのファイルを表すワイルドカード、「-」や「@」ではじまるファイル名の処置、レスポンスファイルの使い方などの相違にも留意する必要がある。

参考資料

  1. 統合アーカイバプロジェクト
  2. 統合アーカイバDLLの総本山。
  3. アンマネージコードとの相互運用
  4. MSDNでは主にこのページ以下にPInvokeに関する解説がある。
  5. Noah
  6. 統合アーカイバDLLを使用して書庫を展開、作成する代表的なソフト。.NET Archiverの作成で、参考にさせていただきました。
  7. DOBON.NET 『遅延バインディングによりアンマネージDLL関数を呼び出す
  8. この記事の基になった記事。
修正履歴

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

どぼん!(ドボン!)

DOBON.NET内で.NET Frameworkの機能を紹介したWebサイト.NET Tipsやメールマガジン「.NETプログラミング研究」の発行人。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/444 2006/07/24 20:04

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー