« Intersect/Union/Inflate/Offset メソッド | トップページ | アンマネージドDLLの動的リンク(2) »

2010年10月16日 (土)

アンマネージドDLLの動的リンク(1)

私事ですが、Delphi製の自作の圧縮解凍アプリを愛用しています。しかし、いかんせん所有する Delphi が Delphi5 (Delphi6 も持っているが、今はインストールさえしていない)なので、Unicodeファイルに対応していません。
そこで、C#移行出来ないかと考えたのが、C#を始めるきっかけになったのです。

当初、.NETでは、動的リンク出来ないという情報も得ていたので、回避策も含めて本当に出来ないのか探した事がありました。そのとき見つけたのが、この方法です。

MoF C#を使ってDLL内の関数を動的に呼び出す方法 …方法1

この方法はC#2003でも使用可能で、実際先程の自作圧縮解凍アプリも、C#2003で作成しはじめた事もあって、うまく動いています。

ところが、C#2005以降では、標準で動的リンクが使える様です。最近までこの事を知りませんでした。こちらの使用方法は、ここなんかに書かれています。

へろぱ的ブログ C#でDLLを動的に呼び出すサンプル …方法2

今は、この圧縮解凍アプリは、C#2005に移行して更に開発中なので、この標準的な使用方法も可能になっています。そこで、ちょっと使用方法の差異等について、比較してみる事にしました。

なお、通常はC#2003で動作確認していますが、今回は方法2が、C#2005以降のみなので、C#2005で動作確認しています。

まずは比較コードを書く準備として、方法1に関しては、上記ホームページ中にクラス化されたソースが載せられているので、これを Get しておく必要があります。この PInvokeMethodGen というクラスは、今回のサンプルソースには含めていませんが、実行には必要です(ないとコンパイルエラーになります)。

また、動的リンクする対象のDLLとして、統合アーカイバプロジェクトの UNLHA32.DLL(Ver.2.40以降) を使用します。これも用意しておく必要があります。

まずは、基本となるコマンドベースでの扱いを見る為、指定された LHA書庫ファイルを一括解凍する、という仕様で下記のサンプルを書いてみました。ここでは、Ansi 文字列(関数 UnlhaA 使用)を前提として、動作させています。

delegate int CommandA(IntPtr hWnd, string szCmdLine, StringBuilder szOutput, int dwSize);
/// <summary>
/// ANSI 文字列によるLHA書庫一括解凍(方法1)
/// 解凍先は、書庫のあるフォルダの下に"1"のサブフォルダを作成して、
/// その下に解凍する。手抜きなので、ドライブ直下だとうまくいかない筈なのに...
/// </summary>
/// <param name="archiveFile">LHA書庫ファイルのフルパス</param>
private void AnsiMelt1(string archiveFile)
{
    using (PInvokeMethodGen pimg = new PInvokeMethodGen("Unlha32.dll"))
    {
        StringBuilder log = new StringBuilder(10240);
        CommandA command = (CommandA)pimg.MethodGen(typeof(CommandA), "UnlhaA");
        string unlhaCommand = String.Format("x {0} {1} *", archiveFile,
            Path.GetDirectoryName(archiveFile) + @"\1\");
        command(this.Handle, unlhaCommand, log, log.Capacity - 1);
    }
}
/// <summary>
/// ANSI 文字列によるLHA書庫一括解凍(方法2)
/// 解凍先は、書庫のあるフォルダの下に"2"のサブフォルダを作成して、
/// その下に解凍する。手抜きなので、ドライブ直下だとうまくいかない筈なのに...
/// </summary>
/// <param name="archiveFile">LHA書庫ファイルのフルパス</param>
private void AnsiMelt2(string archiveFile)
{
    int hModule = LoadLibrary("Unlha32.dll");
    if (hModule == 0) return;
    StringBuilder log = new StringBuilder(10240);
    IntPtr commandAddress = GetProcAddress(hModule, "UnlhaA");
    CommandA command = (CommandA)Marshal.GetDelegateForFunctionPointer(
        commandAddress, typeof(CommandA));
    string unlhaCommand = String.Format("x {0} {1} *", archiveFile,
        Path.GetDirectoryName(archiveFile) + @"\2\");
    command(this.Handle, unlhaCommand, log, log.Capacity - 1);
    FreeLibrary(hModule);
}

方法1の方が、一見簡潔に見えますが、これは PInvokeMethodGen クラスを使用しているおかげです。方法2も、同じ様にして下記のクラスを定義してみました。PInvokeMethodGen と同じ構成(Enabled プロパティも追加してます)でコーディングしています。但し、捨てられたブログの「アンマネージド DLL を遅延バインディングで使用する」の様に、C#2005では、SafeHandle から継承する方が、より実用的な様です。

using System;
using System.Runtime.InteropServices; 

namespace Acha_ya.PracticalClass
{
    /// <summary>
    /// アンマネージドDLLの動的リンクを補助するクラス
    /// </summary>
    public class UnmanagedLink : IDisposable
    {
        [DllImport("kernel32.dll", SetLastError=true)]
        static extern IntPtr LoadLibrary(string lpFileName);
        [DllImport("kernel32.dll")]
        static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
        [DllImport("kernel32.dll")]
        static extern bool FreeLibrary(IntPtr hModule);
        private IntPtr moduleHandle = IntPtr.Zero;
        /// <summary>
        /// 指定されたDLLファイルを、呼び出し側プロセスのアドレス空間内にマップします。
        /// </summary>
        /// <param name="lpFileName">DLLファイル名</param>
        public UnmanagedLink(string lpFileName)
        {
            moduleHandle = LoadLibrary(lpFileName);
        }
        /// <summary>
        /// 動的リンクされているかを取得します。
        /// </summary>
        public bool Enabled
        {
            get
            {
                return (moduleHandle != IntPtr.Zero);
            }
        }
        /// <summary>
        /// 指定した関数をデリゲートに変換する。
        /// </summary>
        /// <param name="delegateType">デリゲートの型</param>
        /// <param name="lpProcName">関数名</param>
        /// <returns>適切なデリゲート型にキャストできるデリゲート インスタンス</returns>
        public Delegate MethodGen(Type delegateType, string lpProcName)
        {
            if (!Enabled) return null;
            IntPtr functionPointer = GetProcAddress(moduleHandle, lpProcName);
            if (functionPointer == IntPtr.Zero)
            {
                int hResult = Marshal.GetHRForLastWin32Error();
                Marshal.ThrowExceptionForHR(hResult);
            }
            return Marshal.GetDelegateForFunctionPointer(functionPointer, delegateType);
        }
        /// <summary>
        /// 動的リンクしているDLLを開放します。
        /// </summary>
        public void Dispose()
        {
            if (Enabled)
            {
                FreeLibrary(moduleHandle);
            }
        }
    }
}

この UnmanagedLink クラス使用を前提にして、先程の AnsiMelt2 メソッドを書き換えた AnsiMelt3 メソッドを作成すれば、

/// <summary>
/// ANSI 文字列によるLHA書庫一括解凍(方法2)
/// 解凍先は、書庫のあるフォルダの下に"2"のサブフォルダを作成して、
/// その下に解凍する。手抜きなので、ドライブ直下だとうまくいかない筈なのに...
/// </summary>
/// <param name="archiveFile">LHA書庫ファイルのフルパス</param>
private void AnsiMelt3(string archiveFile)
{
    using (UnmanagedLink ul = new UnmanagedLink("Unlha32.dll"))
    {
        StringBuilder log = new StringBuilder(10240);
        CommandA command = (CommandA)ul.MethodGen(typeof(CommandA), "UnlhaA");
        string unlhaCommand = String.Format("x {0} {1} *", archiveFile,
            Path.GetDirectoryName(archiveFile) + @"\2\");
        command(this.Handle, unlhaCommand, log, log.Capacity - 1);
    }
}

となり、AnsiMelt1 とほぼ一緒になります。Ansi 文字列では、移植も楽に出来そうです。

ところで、何故 Unicode 文字列でのテストをまずやらなかったかというと、理由があります。PInvokeMethodGen クラスは、制限があって、マーシャリングに対して完全な対応をしていません。MarshalAsAttribute 等の指定が無視されるのです。
従って、Unicode 文字列の場合、単純な string 型で定義しても、うまく動作してくれません。で私の出した結論は、「構造体を使用する」というものでした。string 型のメンバが1つだけの構造体を定義しておき、それを delegate の引数に使用します。もちろん、構造体は StructLayoutAttribute で、CharSet = CharSet.Unicode を指定します。こうして定義した構造体を経由すれば、PInvokeMethodGen クラスでUnicode文字列が使用可能になります。
他にいい手があるのかもしれませんが、冒頭の自作アプリでは、この方式で Unicode 文字列に対処しています。

次回は、このUnicode 文字列での比較、更に 統合アーカイバプロジェクトDLLを使用する上では、欠かせない INDIVIDUALINFO 構造体を使用した API での使用比較を行なってみたいと思います。

最後に、ここまでの動作テスト用サンプルソース一式を、CAB形式で圧縮して、ここに置いておきます。

「DynamicLink.cab」をダウンロード
(※PInvokeMethodGen クラスのソースは、方法1のサイトから取得して下さい)

« Intersect/Union/Inflate/Offset メソッド | トップページ | アンマネージドDLLの動的リンク(2) »

C#研究」カテゴリの記事

コメント

コメントを書く

コメントは記事投稿者が公開するまで表示されません。

(ウェブ上には掲載しません)

トラックバック


この記事へのトラックバック一覧です: アンマネージドDLLの動的リンク(1):

« Intersect/Union/Inflate/Offset メソッド | トップページ | アンマネージドDLLの動的リンク(2) »