DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

遅延バインディングによりアンマネージDLL関数を呼び出す
統合アーカイバ仕様DLLを使ってファイルを圧縮、展開(解凍)する

ここでは.NETで外部のDLLの関数(アンマネージDLL関数)を呼び出す方法を考えます。しかもここでの最終目標は遅延バインディングによる方法です。その具体例として、MiccoさんのUNLHA32.DLLを使って書庫を展開(解凍)するコードを書いてみることにします。最終的には「統合アーカイバプロジェクト」に対応したあらゆるDLLを使って書庫を展開できるアプリケーションを作成することを目的とします。

注意:.NET Framework 2.0からは.NET Frameworkの機能だけでここで紹介していることと同じことができるようになりました。詳しくは、「動的PInvokeによる統合アーカイバ仕様DLLを使用した書庫の展開と作成」をご覧ください。また、統合アーカイバAPI仕様の各DLLのコマンドやスイッチの違いに関して、「統合アーカイバDLLの相違」にまとめてあります。

Unlha32.dllを使って書庫を展開する方法

DLL関数の呼び出し方の説明は後にして、まずはUnlha32.dllを使って書庫を展開する方法をごく簡単に説明します。

Unlha32.dllはLHA(.lzh)形式の書庫を操作(圧縮、解凍など)するためのDLLです。これを使うことにより、簡単に対応書庫の展開が出来るようになります。Unlha32.dllの使い方に関する詳しい説明は、Unlha32.dllのアーカイブに同梱されている各種ドキュメントに詳しく書かれていますので、興味のある方はそちらをお読みください。

Unlha32.dllで書庫を展開するには、通常Unlha関数を使います(Unlha32.dllのアーカイブ内の"API.TXT"に説明があります)。Unlha関数は、コマンド文字列により書庫を操作するもので、そのコマンドはLHA.EXE互換となっています。例えば書庫内のファイルをすべて指定されたフォルダに展開するには、

x 書庫名 解凍先フォルダ名 *

などとします。これにオプション等を付加していけば、展開方法を細かく変えることも出来ます。(コマンドに関してはUnlha32.dllのアーカイブ内の"COMMAND.TXT"に詳しい説明があります。)

展開に関してはこれだけですが、余計なエラーがないように、その前にいくつかのチェックをしておきましょう。まず、展開する前には、Unlha32.dllが本当にインストールされているか、UnlhaGetVersion関数で調べます。また、UNLHA32.DLL は複数同時実行に対応していないので、UnlhaGetRunning関数で実行中でないことを確認します。ついでにUnlhaCheckArchive関数で指定されたファイルがUnlha32.dllで扱える書庫かどうか確認しておきます。

これで書庫の展開に関するUnlha32.dllの知識は身に付いたものとしましょう。

DllImportによるアンマネージDLL関数の呼び出し

.NETでアンマネージDLL関数を呼び出す方法としては、DllImportを使う他ないでしょう(VB.NETのDeclareステートメントも結局は同じことです)。

ここではこの方法を紹介することが目的ではないため、DllImportに関する説明いたしません。これらに関する知識はヘルプの「アンマネージ DLL 関数の処理」や「プラットフォーム呼び出しによるデータのマーシャリング」などでご確認ください。

以降、これらの知識がすでにあるものとして話を進めさせていただきます。

書庫を展開する

以上の知識を基に、いよいよ実際に書庫を展開するコードを書いてみましょう。ここではUNLHA32.DLLで書庫を展開するためのメソッドを作ることにします。このメソッド(LhaExtractArchiveとしましょう)に書庫ファイル名と展開先のフォルダ名を渡すことにより、書庫が展開できます。

VB.NET
コードを隠すコードを選択
'Imports System.Runtime.InteropServices
'がソースファイルの一番上に書かれているものとする

'DLL の版の取得
<DllImport("unlha32")> _
Private Shared Function UnlhaGetVersion() As UInt16
End Function
'DLL の実行状況の取得
<DllImport("unlha32")> _
Private Shared Function UnlhaGetRunning() As Boolean
End Function
'書庫のチェック
<DllImport("unlha32")> _
Private Shared Function UnlhaCheckArchive( _
    ByVal szFileName As String, _
    ByVal iMode As Integer) As Boolean
End Function
'書庫操作一般
<DllImport("unlha32")> _
Private Shared Function Unlha( _
    ByVal hwnd As Integer, _
    ByVal szCmdLine As String, _
    ByVal szOutput As String, _
    ByVal dwSize As Integer) As Integer
End Function

''' <summary>
''' UNLHA32.DLLで書庫を展開する
''' </summary>
''' <param name="archiveFile">書庫ファイル名</param>
''' <param name="extractDir">展開先のフォルダ名</param>
Public Shared Sub LhaExtractArchive( _
        ByVal archiveFile As String, _
        ByVal extractDir As String)
    '指定されたファイルがあるか調べる
    If Not System.IO.File.Exists(archiveFile) Then
        Throw New ApplicationException("ファイルが見つかりません")
    End If

    'DLLのチェック
    Try
        Dim ver As UInt16 = UnlhaGetVersion()
        Console.WriteLine("バージョン:{0}", ver)
    Catch
        Throw New _
            ApplicationException("Unlha32.dllがインストールされていません")
    End Try

    '動作中かチェック
    If UnlhaGetRunning() Then
        Throw New ApplicationException("DLLが動作中")
    End If

    '展開できるかチェック
    If Not UnlhaCheckArchive(archiveFile, 0) Then
        Throw New ApplicationException("対応書庫ではありません")
    End If

    'ファイル名とフォルダ名を修正する
    If archiveFile.IndexOf(" "c) > 0 Then
        archiveFile = """" + archiveFile + """"
    End If
    If Not extractDir.EndsWith("\") Then
        extractDir += "\"
    End If
    If extractDir.IndexOf(" "c) > 0 Then
        extractDir = """" + extractDir + """"
    End If

    '展開する
    Dim ret As Integer = Unlha(0, _
        String.Format("x {0} {1} *", archiveFile, extractDir), _
        Nothing, 0)

    '結果
    If ret <> 0 Then
        Throw New ApplicationException("書庫の展開に失敗しました")
    End If
End Sub
C#
コードを隠すコードを選択
//using System.Runtime.InteropServices;
//がソースファイルの一番上に書かれているものとする

//DLL の版の取得
[DllImport("unlha32")]
private extern static UInt16 UnlhaGetVersion();
//DLL の実行状況の取得
[DllImport("unlha32")]
private extern static bool UnlhaGetRunning();
//書庫のチェック
[DllImport("unlha32")]
private extern static bool UnlhaCheckArchive(string szFileName, 
    int iMode);
//書庫操作一般
[DllImport("unlha32")]
private extern static int Unlha(int hwnd, string szCmdLine, 
    string szOutput, int dwSize);

/// <summary>
/// UNLHA32.DLLで書庫を展開する
/// </summary>
/// <param name="archiveFile">書庫ファイル名</param>
/// <param name="extractDir">展開先のフォルダ名</param>
public static void LhaExtractArchive(
    string archiveFile, string extractDir)
{
    //指定されたファイルがあるか調べる
    if (!System.IO.File.Exists(archiveFile))
        throw new ApplicationException("ファイルが見つかりません");

    //DLLのチェック
    try
    {
        UInt16 ver = UnlhaGetVersion();
        Console.WriteLine("バージョン:{0}", ver);
    }
    catch
    {
        throw new ApplicationException("Unlha32.dllがインストールされていません");
    }

    //動作中かチェック
    if (UnlhaGetRunning())
        throw new ApplicationException("DLLが動作中");

    //展開できるかチェック
    if (!UnlhaCheckArchive(archiveFile, 0))
        throw new ApplicationException("対応書庫ではありません");

    //ファイル名とフォルダ名を修正する
    if (archiveFile.IndexOf(' ') > 0)
        archiveFile = "\"" + archiveFile + "\"";
    if (!extractDir.EndsWith("\\"))
        extractDir += "\\";
    if (extractDir.IndexOf(' ') > 0)
        extractDir = "\"" + extractDir + "\"";

    //展開する
    int ret = Unlha(0,
        string.Format("x {0} {1} *", archiveFile, extractDir),
        null, 0);

    //結果
    if (ret != 0)
        throw new ApplicationException("書庫の展開に失敗しました");
}

うまく行きましたか?普通ならこれで、

「書庫が解凍できました。めでたし、めでたし。」

となって終わるところでしょうが、それでは面白くもなんともありません。さあ、いよいよこれからが本番です。

遅延バインディングの意義

私はこの記事の一番初めに

最終的には「統合アーカイバプロジェクト」に対応したあらゆるDLLを使って書庫を展開できるアプリケーションを作成することを目的とします。

と書きました。さて、UNLHA32.DLL以外のDLLを使って書庫を展開するにはどうすればよいのでしょうか?単純に考えれば、それにはこれらすべてのDLLごとに書庫を展開するためのコードを書いていけばいいとなります。

例えば"UNZIP32.DLL"(shoda T.さん作)の場合は、先のUNLHA32.DLLの例の "UnlhaGetVersion" を "UnZipGetVersion" に、 "UnlhaGetRunning" を "UnZipGetRunning" に、 "UnlhaCheckArchive" を "UnZipCheckArchive" にし、さらに展開のコマンドを "-x 書庫名 解凍先フォルダ名 *" とすればOKでしょう。

"CAB32.DLL"(宮内邦昭さん作)の場合も同様に、UNLHA32.DLLのAPI名の"Unzip"の部分を"Cab"に変え、展開のコマンドを"-x 書庫名 解凍先フォルダ名 *"とします。

"UNZIP32.DLL"と"CAB32.DLL"の例を示しましたが、実は統合アーカイバ仕様のDLLは他も同様に、API名の先頭の文字列とコマンドを変えるだけで同じように扱うことが出来るのです。それがすなわち、「統合アーカイバ仕様」ということです。

このような仕様のおかげで、統合アーカイバ仕様のDLLを使用するコードは、一つを理解すれば後は簡単に書くことが出来ます。しかしそれだけでよいものでしょうか?

今までの考察からすれば、DLLファイル名と、そのDLLのAPIの頭に付く文字列と、展開のためのコマンドが分かれば、そのDLLを使って書庫を展開できうることが分かります。つまり、未知のDLLであってもそれが統合アーカイバ仕様であれば、例えばユーザーから必要な情報を提供されれば、使うことが出来るということになります。これがすなわち、「統合アーカイバプロジェクトに対応したあらゆるDLLを使って書庫を展開できるアプリケーション」ということです。

もしDllImportでDLLファイル名と関数の名前(呼び出すDLLエントリポイントの名前)を変数で指定することができれば、この問題は解決します。しかし残念ながらそれは出来ません。つまり、アプリケーション作成時に使うことを明確にしたDLLしか使うことが出来ない、事前バインディングしか使えないということです。

.NETでは無理ですが、実はVC++ではこれは造作ないことです。LoadLibrary、GetProcAddress関数により動的リンク(ダイナミックリンク)を使うことによって、プロセスの実行コードに含まれない関数を呼び出すことができます。(詳しくはヘルプ「Visual C++ の概念: 機能の追加 - DLL」をご覧ください。)

.NETでも同じことが出来ればよいのですが、果たして可能でしょうか?

遅延バインディングによる方法

その答えが、Richard Birkbyさんが書いた「The Code Project - Late binding on native DLLs with C#」にあります。

詳しくはこのページを読んでいただきたいのですが、つまりは次のようなことのようです。(英語に自信がないので、正確かどうかわかりません。)

Win32 APIのLoadLibrary関数とGetProcAddress関数により指定されたDLLの関数のアドレスは取得できますが、このアドレスで関数にアクセスする方法が.NETにはありません。そこでこの部分は他のDLLでやってもらうことにします。それがここではInvoke.dllのInvokeFunc関数ということになります。ちなみにこのInvokeFunc関数と同じ機能のものに、msjava.dllのcall関数がありますが、msjava.dllはWindows XPにはインストールされていないとのことです。

上記のページからダウンロードできるアーカイバにはソースしか含まれていませんので、Invoke.dllは自分でコンパイルして作成します。makefileがありますので、それをnmake.exe(例えば"C:\Program Files\Microsoft Visual Studio .NET 2003\SDK\v1.1\Bin"にあります)に渡せばコンパイルできます。(私の環境ではそれだけではコンパイルできず、さらにいくつかのディレクトリにパスを通す必要がありました。)

それでは今度はこの方法で先のコードを書き換えてみましょう。展開する書庫名、展開先のほかに、使用するDLL、APIの頭に付く文字列、展開のためのコマンドを引数とするメソッドExtractArchiveを作成します。

VB.NET
コードを隠すコードを選択
'Imports System.Runtime.InteropServices
'がソースファイルの一番上に書かれているものとする

'DLLモジュールをマップ
<DllImport("kernel32")> _
Private Shared Function LoadLibrary( _
    ByVal lpLibFileName As String) As Integer
End Function
'マップを解除
<DllImport("kernel32")> _
Private Shared Function FreeLibrary( _
    ByVal hLibModule As Integer) As Boolean
End Function
'関数のアドレスを取得
<DllImport("kernel32")> _
Private Shared Function GetProcAddress( _
    ByVal hModule As Integer, ByVal lpProcName As String) As Integer
End Function

'以下使用するAPIのためのInvokeFunc
<DllImport("Invoke", EntryPoint:="InvokeFunc")> _
Private Shared Function InvokeGetVersion( _
    ByVal funcptr As Integer) As UInt16
End Function
<DllImport("Invoke", EntryPoint:="InvokeFunc")> _
Private Shared Function InvokeGetRunning( _
    ByVal funcptr As Integer) As Boolean
End Function
<DllImport("Invoke", EntryPoint:="InvokeFunc")> _
Private Shared Function InvokeCheckArchive( _
    ByVal funcptr As Integer, _
    ByVal szFileName As String, _
    ByVal iMode As Integer) As Boolean
End Function
<DllImport("Invoke", EntryPoint:="InvokeFunc")> _
Private Shared Function InvokeMain( _
    ByVal funcptr As Integer, _
    ByVal hwnd As Integer, _
    ByVal szCmdLine As String, _
    ByVal szOutput As String, _
    ByVal dwSize As Integer) As Integer
End Function

''' <summary>
''' 統合アーカイバ仕様のDLLで書庫を展開する
''' </summary>
''' <param name="dllName">DLLファイル名</param>
''' <param name="funcName">APIの頭に付く文字列</param>
''' <param name="command">展開のためのコマンド</param>
''' <param name="archiveFile">書庫ファイル名</param>
''' <param name="extractDir">展開先のフォルダ名</param>
Public Shared Sub ExtractArchive( _
        ByVal dllName As String, _
        ByVal funcName As String, _
        ByVal command As String, _
        ByVal archiveFile As String, _
        ByVal extractDir As String)
    '指定されたファイルがあるか調べる
    If Not System.IO.File.Exists(archiveFile) Then
        Throw New ApplicationException("ファイルが見つかりません")
    End If
    'DLLをロード
    Dim hmod As Integer = LoadLibrary(dllName)
    If hmod = 0 Then
        Throw New ApplicationException( _
            dllName + "のロードに失敗しました")
    End If
    Try
        Dim funcaddr As Integer

        'DLLのチェック
        '関数のアドレスを取得
        funcaddr = GetProcAddress(hmod, funcName + "GetVersion")
        If funcaddr = 0 Then
            Throw New ApplicationException( _
                dllName + "がインストールされていません")
        End If
        Dim ver As UInt16 = InvokeGetVersion(funcaddr)
        Console.WriteLine("バージョン:{0}", ver)

        '動作中かチェック
        funcaddr = GetProcAddress(hmod, funcName + "GetRunning")
        If funcaddr = 0 Then
            Throw New ApplicationException( _
                funcName + "GetRunningのアドレスが取得できませんでした")
        End If
        If InvokeGetRunning(funcaddr) Then
            Throw New ApplicationException(dllName + "が動作中")
        End If

        '展開できるかチェック
        funcaddr = GetProcAddress(hmod, funcName + "CheckArchive")
        If funcaddr = 0 Then
            Throw New ApplicationException( _
                funcName + "CheckArchiveのアドレスが取得できませんでした")
        End If
        If Not InvokeCheckArchive(funcaddr, archiveFile, 0) Then
            Throw New ApplicationException( _
                archiveFile + "は対応書庫ではありません")
        End If

        'ファイル名とフォルダ名を修正する
        If archiveFile.IndexOf(" "c) > 0 Then
            archiveFile = """" + archiveFile + """"
        End If
        If Not extractDir.EndsWith("\") Then
            extractDir += "\"
        End If
        If extractDir.IndexOf(" "c) > 0 Then
            extractDir = """" + extractDir + """"
        End If

        '展開する
        funcaddr = GetProcAddress(hmod, funcName)
        If funcaddr = 0 Then
            Throw New ApplicationException( _
                funcName + "のアドレスが取得できませんでした")
        End If
        Dim ret As Integer = InvokeMain(funcaddr, 0, _
            String.Format(command, archiveFile, extractDir), _
            Nothing, 0)

        '結果
        If ret <> 0 Then
            Throw New ApplicationException("書庫の展開に失敗しました")
        End If
    Finally
        '解放する
        If hmod <> 0 Then
            FreeLibrary(hmod)
        End If
    End Try
End Sub
C#
コードを隠すコードを選択
//using System.Runtime.InteropServices;
//がソースファイルの一番上に書かれているものとする

//DLLモジュールをマップ
[DllImport("kernel32")]
private extern static int LoadLibrary(string lpLibFileName);
//マップを解除
[DllImport("kernel32")]
private extern static bool FreeLibrary(int hLibModule);
//関数のアドレスを取得
[DllImport("kernel32")]
private extern static int GetProcAddress(int hModule,
    string lpProcName);
//以下使用するAPIのためのInvokeFunc
[DllImport("Invoke", EntryPoint="InvokeFunc")]
privatelic extern static UInt16 InvokeGetVersion(int funcptr);
[DllImport("Invoke", EntryPoint="InvokeFunc")]
private extern static bool InvokeGetRunning(int funcptr);
[DllImport("Invoke", EntryPoint="InvokeFunc")]
private extern static bool InvokeCheckArchive(int funcptr,
    string szFileName, int iMode);
[DllImport("Invoke", EntryPoint="InvokeFunc")]
private extern static int InvokeMain(int funcptr, int hwnd,
    string szCmdLine, string szOutput, int dwSize);

/// <summary>
/// 統合アーカイバ仕様のDLLで書庫を展開する
/// </summary>
/// <param name="dllName">DLLファイル名</param>
/// <param name="funcName">APIの頭に付く文字列</param>
/// <param name="command">展開のためのコマンド</param>
/// <param name="archiveFile">書庫ファイル名</param>
/// <param name="extractDir">展開先のフォルダ名</param>
public static void ExtractArchive(string dllName, string funcName,
    string command, string archiveFile, string extractDir)
{
    //指定されたファイルがあるか調べる
    if (!System.IO.File.Exists(archiveFile))
        throw new ApplicationException("ファイルが見つかりません");

    //DLLをロード
    int hmod = LoadLibrary(dllName);
    if (hmod == 0)
        throw new ApplicationException(dllName + "のロードに失敗しました");

    try
    {
        int funcaddr;

        //DLLのチェック
        //関数のアドレスを取得
        funcaddr = GetProcAddress(hmod, funcName + "GetVersion");
        if (funcaddr == 0)
            throw new ApplicationException(
                dllName + "がインストールされていません");
        UInt16 ver = InvokeGetVersion(funcaddr);
        Console.WriteLine("バージョン:{0}", ver);

        //動作中かチェック
        funcaddr = GetProcAddress(hmod, funcName + "GetRunning");
        if (funcaddr == 0)
            throw new ApplicationException(
                funcName + "GetRunningのアドレスが取得できませんでした");
        if (InvokeGetRunning(funcaddr))
            throw new ApplicationException(dllName + "が動作中");

        //展開できるかチェック
        funcaddr = GetProcAddress(hmod, funcName + "CheckArchive");
        if (funcaddr == 0)
            throw new ApplicationException(
                funcName + "CheckArchiveのアドレスが取得できませんでした");
        if (!InvokeCheckArchive(funcaddr, archiveFile, 0))
            throw new ApplicationException(
                archiveFile + "は対応書庫ではありません");

        //ファイル名とフォルダ名を修正する
        if (archiveFile.IndexOf(' ') > 0)
            archiveFile = "\"" + archiveFile + "\"";
        if (!extractDir.EndsWith("\\"))
            extractDir += "\\";
        if (extractDir.IndexOf(' ') > 0)
            extractDir = "\"" + extractDir + "\"";

        //展開する
        funcaddr = GetProcAddress(hmod, funcName);
        if (funcaddr == 0)
            throw new ApplicationException(
                funcName + "のアドレスが取得できませんでした");
        int ret = InvokeMain(funcaddr, 0, 
            string.Format(command, archiveFile, extractDir),
            null, 0);

        //結果
        if (ret != 0)
            throw new ApplicationException("書庫の展開に失敗しました");
    }
    finally
    {
        //解放する
        if (hmod != 0)
            FreeLibrary(hmod);
    }
}

これでユーザーがこれらの情報を与えることですべての統合アーカイバ仕様のDLLに対応したアプリが作れるようになりました。

ついに完成!

一応の目標は達成したといえますが、せっかくここまで来たので、もう少しまともなアプリを作りましょう。

書庫の展開に必要なDLLに関する情報はユーザーがXML形式のファイルで与えるものとし、アプリは書庫を展開する時にその情報を読み込み、どのDLLで展開するのが適当かアプリ自身が判断するようにします。

ユーザーが与えるXMLファイルは例えば次のように記述するものとします。このようにすれば、アプリでは逆シリアル化により、簡単に情報を読み込むことが出来るようになります。このファイルは実行ファイルと同じフォルダにファイル名"dlls.config"という名前でUTF-8で保存されているものとします。

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfArchiverDllInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <ArchiverDllInfo>
    <FileName>unlha32</FileName>
    <FunctionName>Unlha</FunctionName>
    <CommandToExtract>x {0} {1} *</CommandToExtract>
  </ArchiverDllInfo>
  <ArchiverDllInfo>
    <FileName>unzip32</FileName>
    <FunctionName>UnZip</FunctionName>
    <CommandToExtract>-x {0} {1} *</CommandToExtract>
  </ArchiverDllInfo>
  <ArchiverDllInfo>
    <FileName>cab32</FileName>
    <FunctionName>Cab</FunctionName>
    <CommandToExtract>-x {0} {1} *</CommandToExtract>
  </ArchiverDllInfo>
</ArrayOfArchiverDllInfo>

次の例はコンソールアプリです。コマンドライン引数に展開したいファイルを指定すると、「デスクトップ + 書庫ファイルの名前」というフォルダに展開します。

VB.NET
コードを隠すコードを選択
'C#のコードを'C# to VB.NET Translator'で変換し、修正したコードです
'http://www.aspalliance.com/aldotnet/examples/translate.aspx
Imports System
Imports System.Runtime.InteropServices

Namespace Dobon.Sample.File
    Public Class ExtractArchiveWithDll
        Public Shared Sub Main(ByVal args() As String)
            Dim arg As String
            For Each arg In args
                '展開先のフォルダ名を決める
                'ここではデスクトップ上の書庫ファイル名のフォルダとする
                Dim extractDir As String = _
                    System.Environment.GetFolderPath( _
                    System.Environment.SpecialFolder.DesktopDirectory)
                extractDir += "\" + _
                    System.IO.Path.GetFileNameWithoutExtension(arg)
                '存在しないフォルダ名を探す
                If System.IO.Directory.Exists(extractDir) Then
                    Dim n As Integer = 1
                    While System.IO.Directory.Exists( _
                            extractDir + n.ToString())
                        n += 1
                    End While
                    extractDir += n.ToString()
                End If

                Console.WriteLine("""{0}""を""{1}""に展開します...", _
                    arg, extractDir)

                '展開する
                Try
                    If ExtractArchiveEx(arg, extractDir) Then
                        Console.WriteLine("展開しました。")
                        'フォルダを開く
                        System.Diagnostics.Process.Start(extractDir)
                    Else
                        Console.WriteLine("展開できませんでした。")
                    End If
                Catch ex As Exception
                    Console.WriteLine(("エラー:" + ex.Message))
                End Try
            Next arg

            Console.ReadLine()
        End Sub

        'DLLモジュールをマップ
        <DllImport("kernel32")> _
        Private Shared Function LoadLibrary( _
            ByVal lpLibFileName As String) As Integer
        End Function
        'マップを解除
        <DllImport("kernel32")> _
        Private Shared Function FreeLibrary( _
            ByVal hLibModule As Integer) As Boolean
        End Function
        '関数のアドレスを取得
        <DllImport("kernel32")> _
        Private Shared Function GetProcAddress( _
            ByVal hModule As Integer, _
            ByVal lpProcName As String) As Integer
        End Function

        '以下使用するAPIのためのInvokeFunc
        <DllImport("Invoke", EntryPoint:="InvokeFunc")> _
        Private Shared Function InvokeGetVersion( _
            ByVal funcptr As Integer) As UInt16
        End Function
        <DllImport("Invoke", EntryPoint:="InvokeFunc")> _
        Private Shared Function InvokeGetRunning( _
            ByVal funcptr As Integer) As Boolean
        End Function
        <DllImport("Invoke", EntryPoint:="InvokeFunc")> _
        Private Shared Function InvokeCheckArchive( _
            ByVal funcptr As Integer, _
            ByVal szFileName As String, _
            ByVal iMode As Integer) As Boolean
        End Function
        <DllImport("Invoke", EntryPoint:="InvokeFunc")> _
        Private Shared Function InvokeMain( _
            ByVal funcptr As Integer, _
            ByVal hwnd As Integer, _
            ByVal szCmdLine As String, _
            ByVal szOutput As String, _
            ByVal dwSize As Integer) As Integer
        End Function

        'DLLの情報
        Public Structure ArchiverDllInfo
            Public FileName As String           'DLLファイル名
            Public FunctionName As String       'APIの頭に付く文字列
            Public CommandToExtract As String   '展開のためのコマンド
        End Structure 'ArchiverDllInfo

        ''' <summary>
        ''' 統合アーカイバ仕様のDLLで書庫を展開する
        ''' </summary>
        ''' <param name="archiveFile">書庫ファイル名</param>
        ''' <param name="extractDir">展開先のフォルダ名</param>
        ''' <returns>展開できたらTrue</returns>
        Public Shared Function ExtractArchiveEx( _
                ByVal archiveFile As String, _
                ByVal extractDir As String) As Boolean
            '指定されたファイルがあるか調べる
            If Not System.IO.File.Exists(archiveFile) Then
                Throw New ApplicationException("ファイルが見つかりません")
            End If

            'DLL情報ファイルの存在を確認
            Dim dllInfoFile As String = GetAppPath() + "\dlls.config"
            If Not System.IO.File.Exists(dllInfoFile) Then
                Throw New ApplicationException( _
                    "DLL情報ファイルが見つかりません")
            End If
            'DLLの情報を読み込む
            Dim serializer As _
                New System.Xml.Serialization.XmlSerializer( _
                    GetType(ArchiverDllInfo()))
            Dim sr As New System.IO.StreamReader( _
                fileName, System.Text.Encoding.UTF8)
            Dim dllInfos() As ArchiverDllInfo
            dllInfos = CType(serializer.Deserialize(sr), _
                ArchiverDllInfo())
            sr.Close()
            If dllInfos Is Nothing Or dllInfos.Length = 0 Then
                Throw New ApplicationException( _
                    "DLL情報が読み込めませんでした")
            End If

            Dim di As ArchiverDllInfo
            For Each di In dllInfos
                Dim dllName As String = di.FileName
                Dim funcName As String = di.FunctionName

                'DLLをロード
                Dim hmod As Integer = LoadLibrary(dllName)
                If hmod = 0 Then
                    GoTo ContinueForEach1
                End If
                Try
                    Dim funcaddr As Integer

                    'DLLのチェック
                    '関数のアドレスを取得
                    funcaddr = GetProcAddress(hmod, _
                        funcName + "GetVersion")
                    If funcaddr = 0 Then
                        GoTo ContinueForEach1
                    End If
                    Dim ver As UInt16 = InvokeGetVersion(funcaddr)

                    '展開できるかチェック
                    funcaddr = GetProcAddress(hmod, _
                        funcName + "CheckArchive")
                    If funcaddr = 0 Then
                        GoTo ContinueForEach1
                    End If
                    If Not InvokeCheckArchive(funcaddr, _
                            archiveFile, 0) Then
                        GoTo ContinueForEach1
                    End If
                    Console.WriteLine("対応DLLは{0}です", dllName)

                    '動作中かチェック
                    funcaddr = GetProcAddress(hmod, _
                        funcName + "GetRunning")
                    If funcaddr = 0 Then
                        GoTo ContinueForEach1
                    End If
                    If InvokeGetRunning(funcaddr) Then
                        Throw New ApplicationException( _
                            dllName + "が動作中です")
                    End If

                    'ファイル名とフォルダ名を修正する
                    If archiveFile.IndexOf(" "c) > 0 Then
                        archiveFile = """" + archiveFile + """"
                    End If
                    If Not extractDir.EndsWith("\") Then
                        extractDir += "\"
                    End If
                    If extractDir.IndexOf(" "c) > 0 Then
                        extractDir = """" + extractDir + """"
                    End If

                    '展開する
                    funcaddr = GetProcAddress(hmod, funcName)
                    If funcaddr = 0 Then
                        Throw New ApplicationException( _
                            funcName + "のアドレスが取得できませんでした")
                    End If
                    Dim ret As Integer = InvokeMain(funcaddr, 0, _
                        String.Format(di.CommandToExtract, archiveFile, _
                        extractDir), Nothing, 0)

                    '結果
                    If ret <> 0 Then
                        Throw New ApplicationException( _
                            dllName + "での書庫の展開に失敗しました")
                    Else
                        Return True
                    End If
                Finally
                    '解放する
                    If hmod <> 0 Then
                        FreeLibrary(hmod)
                    End If
                End Try
ContinueForEach1:
            Next di
            Return False
        End Function


        Private Shared Function GetAppPath() As String
            Dim fi As New System.IO.FileInfo( _
                System.Reflection.Assembly.GetExecutingAssembly().Location)
            Return fi.DirectoryName
        End Function
    End Class
End Namespace
C#
コードを隠すコードを選択
using System;
using System.Runtime.InteropServices;

namespace Dobon.Sample.File
{
    public class ExtractArchiveWithDll
    {
        public static void Main(string[] args)
        {
            foreach (string arg in args)
            {
                //展開先のフォルダ名を決める
                //ここではデスクトップ上の書庫ファイル名のフォルダとする
                string extractDir = 
                    System.Environment.GetFolderPath(
                    System.Environment.SpecialFolder.DesktopDirectory);
                extractDir += "\\" + 
                    System.IO.Path.GetFileNameWithoutExtension(arg);
                //存在しないフォルダ名を探す
                if (System.IO.Directory.Exists(extractDir))
                {
                    int n = 0;
                    while (System.IO.Directory.Exists(extractDir 
                                                + (++n).ToString()));
                    extractDir += n.ToString();
                }

                Console.WriteLine("\"{0}\"を\"{1}\"に展開します...", 
                    arg, extractDir);

                //展開する
                try
                {
                    if (ExtractArchiveEx(arg, extractDir))
                    {
                        Console.WriteLine("展開しました。");
                        //フォルダを開く
                        System.Diagnostics.Process.Start(extractDir);
                    }
                    else
                        Console.WriteLine("展開できませんでした。");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("エラー:" + ex.Message);
                }
            }

            Console.ReadLine();
        }

        //DLLモジュールをマップ
        [DllImport("kernel32")]
        private extern static int LoadLibrary(string lpLibFileName);
        //マップを解除
        [DllImport("kernel32")]
        private extern static bool FreeLibrary(int hLibModule);
        //関数のアドレスを取得
        [DllImport("kernel32")]
        private extern static int GetProcAddress(
            int hModule, string lpProcName);
        //以下使用するAPIのためのInvokeFunc
        [DllImport("Invoke", EntryPoint="InvokeFunc")]
        private extern static UInt16 InvokeGetVersion(int funcptr);
        [DllImport("Invoke", EntryPoint="InvokeFunc")]
        private extern static bool InvokeGetRunning(int funcptr);
        [DllImport("Invoke", EntryPoint="InvokeFunc")]
        private extern static bool InvokeCheckArchive(
            int funcptr, string szFileName, int iMode);
        [DllImport("Invoke", EntryPoint="InvokeFunc")]
        private extern static int InvokeMain(
            int funcptr, int hwnd, string szCmdLine, 
            string szOutput, int dwSize);

        //DLLの情報
        public struct ArchiverDllInfo
        {
            public string FileName;            //DLLファイル名
            public string FunctionName;        //APIの頭に付く文字列
            public string CommandToExtract;    //展開のためのコマンド
        }

        /// <summary>
        /// 統合アーカイバ仕様のDLLで書庫を展開する
        /// </summary>
        /// <param name="archiveFile">書庫ファイル名</param>
        /// <param name="extractDir">展開先のフォルダ名</param>
        /// <returns>展開できたらTrue</returns>
        public static bool ExtractArchiveEx(
            string archiveFile, string extractDir)
        {
            //指定されたファイルがあるか調べる
            if (!System.IO.File.Exists(archiveFile))
                throw new ApplicationException(
                    "ファイルが見つかりません");

            //DLL情報ファイルの存在を確認
            string dllInfoFile =
                GetAppPath() + "\\dlls.config";
            if (!System.IO.File.Exists(dllInfoFile))
                throw new ApplicationException(
                    "DLL情報ファイルが見つかりません");
            //DLLの情報を読み込む
            System.Xml.Serialization.XmlSerializer serializer =
                new System.Xml.Serialization.XmlSerializer(
                    typeof(ArchiverDllInfo[]));
            System.IO.StreamReader sr = new System.IO.StreamReader(
                fileName, System.Text.Encoding.UTF8);
            ArchiverDllInfo[] dllInfos;
            dllInfos = (ArchiverDllInfo[]) serializer.Deserialize(sr);
            sr.Close();
            if (dllInfos == null || dllInfos.Length == 0)
                throw new ApplicationException(
                    "DLL情報が読み込めませんでした");

            foreach (ArchiverDllInfo di in dllInfos)
            {
                string dllName = di.FileName;
                string funcName = di.FunctionName;

                //DLLをロード
                int hmod = LoadLibrary(dllName);
                if (hmod == 0)
                    continue;

                try
                {
                    int funcaddr;

                    //DLLのチェック
                    //関数のアドレスを取得
                    funcaddr = GetProcAddress(
                        hmod, funcName + "GetVersion");
                    if (funcaddr == 0)
                        continue;
                    UInt16 ver = InvokeGetVersion(funcaddr);
                    if (ver == 0)
                        continue;

                    //展開できるかチェック
                    funcaddr = GetProcAddress(
                        hmod, funcName + "CheckArchive");
                    if (funcaddr == 0)
                        continue;
                    if (!InvokeCheckArchive(funcaddr, archiveFile, 0))
                        continue;
                    Console.WriteLine("対応DLLは{0}です", dllName);

                    //動作中かチェック
                    funcaddr = GetProcAddress(
                        hmod, funcName + "GetRunning");
                    if (funcaddr == 0)
                        continue;
                    if (InvokeGetRunning(funcaddr))
                        throw new ApplicationException(
                            dllName + "が動作中です");

                    //ファイル名とフォルダ名を修正する
                    if (archiveFile.IndexOf(' ') > 0)
                        archiveFile = "\"" + archiveFile + "\"";
                    if (!extractDir.EndsWith("\\"))
                        extractDir += "\\";
                    if (extractDir.IndexOf(' ') > 0)
                        extractDir = "\"" + extractDir + "\"";

                    //展開する
                    funcaddr = GetProcAddress(hmod, funcName);
                    if (funcaddr == 0)
                        throw new ApplicationException(
                            funcName + "のアドレスが取得できませんでした");
                    int ret = InvokeMain(funcaddr, 0, 
                        string.Format(di.CommandToExtract,
                            archiveFile, extractDir),
                        null, 0);

                    //結果
                    if (ret != 0)
                        throw new ApplicationException(
                            dllName + "での書庫の展開に失敗しました");
                    else
                        return true;
                }
                finally
                {
                    //解放する
                    if (hmod != 0)
                        FreeLibrary(hmod);
                }
            }
            return false;
        }

        private static string GetAppPath()
        {
            System.IO.FileInfo fi =
                new System.IO.FileInfo(
                System.Reflection.Assembly.GetExecutingAssembly().Location);
            return fi.DirectoryName;
        }
    }
}
  • 履歴:
  • 2014/5/12 XML逆シリアル化する時、FileStreamの代わりにStreamReaderを使うようにした。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • Windows Vista以降でUACが有効になっていると、ファイルへの書き込みに失敗する可能性があります。詳しくは、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。