« 処理時間の計測 | トップページ | マネージドDLLの動的リンク(2) »

2010年11月11日 (木)

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

以前にアンマネージドDLLの動的リンクについて述べましたが、マネージドDLLの動的リンクはどうやるのかについて書きます。

アンマネージドDLLの動的リンクは、統合アーカイバプロジェクト関連のDLLの使用が目的でした。一方、マネージドDLL として、ZIP書庫を操作できる SharpZipLib というのが公開されています。この機能調査も含めて、動的リンクをしたらどうなるかも一緒にやってみました。

第1回目のこの回では、まずオーソドックスな方法(?)による動的リンクを使った方法に関する記事とします。

まず、マネージドDLLの動的リンクについての参考にしたところです。

Slashdot fslashtさんの日記 「[C#] アセンブリを動的に参照したい」
http://slashdot.jp/~fslasht/journal/467031

ここで触れられているのは、ごく単純な構造のメソッドですが、参考になると思います。通常の参照設定による静的リンクと比べると、多少面倒になりますが、充分実用的です。しかし、SharpZipLib の様な本格的なもののメソッド利用を前提にした場合だと、使用感はどうなるでしょうか。

そこで、今回静的リンクと動的リンクによる、同一機能を使用したコードを書いてみました。まずあらかじめ断っておきますが、動的リンク自体は、Assembly.LoadFile 一発で出来ます。今回は、純粋な動的リンク部分は、以下の様に Form の Load イベント時に処理した後は、そのままです。これはリンクの開放は、普通には出来ないからです。

private Assembly zip;   // 動的リンク用の SharpZipLib.dll アセンブリ格納用
private void Form1_Load(object sender, EventArgs e)
{
    try
    {
        string dllPath = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath),
            "ICSharpCode.SharpZipLib.dll");
        zip = Assembly.LoadFile(dllPath);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message, ex.Source, MessageBoxButtons.OK, MessageBoxIcon.Stop);
        btnList.Enabled = false;
    }
}

マネージドDLLを動的リンクする事による問題は、リンク方法そのものではなく、その副作用として SharpZipLib で独自に定義したクラスや列挙体といったものが、(静的には)使えなくなる事にあります。つまり、型情報を動的に呼び出す必要が出てきます。
なお、型情報の動的呼び出しについては、DOBON.NET 「型のメンバを動的に呼び出す」に、詳細に書かれています。

1.ZIP書庫メンバ削除による比較

まず ZIP書庫の指定メンバを削除するコードです。「いきなり削除かい」と突っ込まれそうですが、面倒なメソッドから始めるよりは、いいかと思ってこれを最初にしました。
簡単に説明すると、使用するクラスは ZipFile というクラスで、コンストラクタでZIP書庫パスを指定してインスタンスを生成します。その後、BeginUpdate → Delete → CommitUpdate メソッドを実行します。Delete メソッドには、ZipEntry 指定と メンバ名指定の2種類があるのですが、何故か ZipEntry 指定時は戻り値なしなので、メンバ名指定の方で行なう用にしました(動的リンク時もその方が楽なせいもある)。BeginUpdate 後から CommitUpdate までの間に、例外発生時は、AbortUpdate メソッドを実行する様にしています。そして、最後に、Close メソッド実行です。本当は、try ~ finally で Close すべきなのでしょうが、リンク方式の違いによる差異はない部分なので入れてません。

/// <summary>
/// 静的リンクによる書庫メンバ削除
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
/// <param name="member">削除するメンバ名</param>
private static void DeleteMember1(string archiveFile, string member)
{
    ICSharpCode.SharpZipLib.Zip.ZipFile zipFile =
        new ICSharpCode.SharpZipLib.Zip.ZipFile(archiveFile);
    bool result = false;
    zipFile.BeginUpdate();
    try
    {
        result = zipFile.Delete(member);
        if (!result)
            MessageBox.Show(member + "の削除に失敗しました");
        zipFile.CommitUpdate();
    }
    catch (Exception e)
    {
        zipFile.AbortUpdate();
        MessageBox.Show(e.Message, e.Source, MessageBoxButtons.OK, MessageBoxIcon.Stop);
    }
    zipFile.Close();
}

/// <summary>
/// 動的リンクによる書庫メンバ削除
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
/// <param name="member">削除するメンバ名</param>
private void DeleteMember2(string archiveFile, string member)
{
    Type zipFile = zip.GetType("ICSharpCode.SharpZipLib.Zip.ZipFile", true, false);
    object zf = zipFile.InvokeMember(null, BindingFlags.CreateInstance, null, null,
        new object[] { archiveFile });
    bool result = false;
    zipFile.InvokeMember("BeginUpdate", BindingFlags.InvokeMethod, null, zf, null);
    try
    {
        result = (bool)zipFile.InvokeMember("Delete", BindingFlags.InvokeMethod,
            null, zf, new object[] { member });
        if (!result)
            MessageBox.Show(member + "の削除に失敗しました");
        zipFile.InvokeMember("CommitUpdate", BindingFlags.InvokeMethod, null, zf, null);
    }
    catch (Exception e)
    {
        zipFile.InvokeMember("AbortUpdate", BindingFlags.InvokeMethod, null, zf, null);
        if (e.InnerException == null)
            MessageBox.Show(e.Message, e.Source, MessageBoxButtons.OK, MessageBoxIcon.Stop);
        else
            MessageBox.Show(e.InnerException.Message, e.InnerException.Source,
                MessageBoxButtons.OK, MessageBoxIcon.Stop);
    }
    zipFile.InvokeMember("Close", BindingFlags.InvokeMethod, null, zf, null);
}

コンストラクタと Delete メソッド実行時に引数が必要です。動的リンクのときは見てのとおり、new object[] { archiveFile } といった風に記述すればいいです。あとは、ほぼ参考元と同じ様な感じですが、例外メッセージの処理だけ違う点に注意して下さい。InnerException を見ないと、本来の例外メッセージが表示されません。

2.ZIP書庫へのメンバ追加による比較

書庫へのメンバ追加も、全体としては削除と同じ様な感じの記述となります。Add メソッドも、引数の指定にバリエーションがありますが、Unicode ファイル対応と、従来の Unicode 非対応アーカイバとの互換を考慮した場合、useUnicodeText パラメータの指定は不可欠かと思います。なので、この指定が出来る Add メソッドを利用してます(但しサンプルでは false 固定で指定してます)。

/// <summary>
/// 静的リンクによるZIP書庫へのメンバ追加
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
/// <param name="addFiles">追加対象となるファイルパスの配列</param>
private static void AddMember1(string archiveFile, string[] addFiles)
{
    ICSharpCode.SharpZipLib.Zip.ZipFile zipFile =
        new ICSharpCode.SharpZipLib.Zip.ZipFile(archiveFile);
    zipFile.BeginUpdate();
    try
    {
        foreach (string file in addFiles)
        {
            string member = Path.GetFileName(file); // ファイル名をエントリ名にする
            // メンバを追加する
            // useUnicodeText は、ファイル名の Shift-JIS 表現が可能な限り false 指定の方が互換性がよい
            zipFile.Add(file, ICSharpCode.SharpZipLib.Zip.CompressionMethod.Deflated, false);
        }
        zipFile.CommitUpdate();
    }
    catch (Exception e)
    {
        zipFile.AbortUpdate();
        MessageBox.Show(e.Message, e.Source, MessageBoxButtons.OK, MessageBoxIcon.Stop);
    }
    zipFile.Close();
}
/// <summary>
/// 動的リンクによるZIP書庫へのメンバ追加
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
/// <param name="addFiles">追加対象となるファイルパスの配列</param>
private void AddMember2(string archiveFile, string[] addFiles)
{
    Type zipFile = zip.GetType("ICSharpCode.SharpZipLib.Zip.ZipFile", true, false);
    Type compressionMethod = zip.GetType("ICSharpCode.SharpZipLib.Zip.CompressionMethod", true, false);
    object zf = zipFile.InvokeMember(null, BindingFlags.CreateInstance, null, null,
        new object[] { archiveFile });
    zipFile.InvokeMember("BeginUpdate", BindingFlags.InvokeMethod, null, zf, null);
    try
    {
        foreach (string file in addFiles)
        {
            string member = Path.GetFileName(file); // ファイル名をエントリ名にする
            zipFile.InvokeMember("Add", BindingFlags.InvokeMethod, null, zf,
                new object[] { file, compressionMethod.GetField("Deflated").GetValue(zf), false });
        }
        zipFile.InvokeMember("CommitUpdate", BindingFlags.InvokeMethod, null, zf, null);
    }
    catch (Exception e)
    {
        zipFile.InvokeMember("AbortUpdate", BindingFlags.InvokeMethod, null, zf, null);
        if (e.InnerException == null)
            MessageBox.Show(e.Message, e.Source, MessageBoxButtons.OK, MessageBoxIcon.Stop);
        else
            MessageBox.Show(e.InnerException.Message, e.InnerException.Source,
                MessageBoxButtons.OK, MessageBoxIcon.Stop);
    }
    zipFile.InvokeMember("Close", BindingFlags.InvokeMethod, null, zf, null);
}

ここでの注意点は、Add メソッドの第2引数が、独自に定義された列挙型を使用している点です。この ICSharpCode.SharpZipLib.Zip.CompressionMethod 列挙型を使用する為に、変数 compressionMethod へのType取得を行ない、Deflated メンバ値を、compressionMethod.GetField("Deflated").GetValue(zf) にて指定しています。当初メソッド引数の指定は object 配列なので、Deflated の値となる 8 を直接指定しても問題ないだろうと思ったのですが、これでは Add メソッドが見つからないとの例外が発生してしまいました。よくよく考えれば、Add メソッドはオーバーロードされていますので、引数の型によりどの Add メソッドを使用するかが決定されています。その為、きちんと型を意識した引数指定が必要だという事です。
結果的に、メンバ削除のコードよりはリンク方式の違いによる差異が、より大きくなっています。

3.ZIP書庫内のメンバ一覧取得

書庫一覧取得に、ZipFile クラスを使用しました。しかし実際のメンバ情報は、ZipEntry クラスに格納されます。サンプルでは、この ZipEntry オブジェクトをそのまま、ListBox の各 Item に格納する様にしています。今度は、例外処理を入れてないせいもあり、静的リンクによる一覧取得は、一番シンプルです。

/// <summary>
/// 静的リンクによる書庫一覧取得・表示
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
private void FileList1(string archiveFile)
{
    listBox1.Items.Clear();
    ICSharpCode.SharpZipLib.Zip.ZipFile zipFile =
        new ICSharpCode.SharpZipLib.Zip.ZipFile(archiveFile);
    foreach (ICSharpCode.SharpZipLib.Zip.ZipEntry ze in zipFile)
    {
        listBox1.Items.Add(ze);
    }
    zipFile.Close();
    return;
}

/// <summary>
/// 動的リンクによる書庫一覧取得・表示
/// </summary>
/// <param name="archiveFile">ZIP書庫ファイルのフルパス</param>
private void FileList2(string archiveFile)
{
    listBox2.Items.Clear();
    Type zipFile = zip.GetType("ICSharpCode.SharpZipLib.Zip.ZipFile", true, false);
    Type zipEntry = zip.GetType("ICSharpCode.SharpZipLib.Zip.ZipEntry", true, false);
    object zf = zipFile.InvokeMember(null, BindingFlags.CreateInstance, null, null,
        new object[] { archiveFile });
    IEnumerator ienum = (IEnumerator)zipFile.InvokeMember("GetEnumerator",
        BindingFlags.InvokeMethod, null, zf, null);
    while (ienum.MoveNext())
    {
        object ze = ienum.Current;
        listBox2.Items.Add(ze);
    }
    zipFile.InvokeMember("Close", BindingFlags.InvokeMethod, null, zf, null);
}

静的リンク時のコードがシンプルなせいで目立ちませんが、今度は、静的リンクと動的リンクのコードの差異がかなり大きいです。静的リンクでは、ICSharpCode.SharpZipLib.Zip.ZipFile クラスのインスタンスを生成したら、それをそのまま foreach で回して、ZipEntry を取得出来ましたが、動的リンクだとこうはいきません。といっても複雑だという程にも見えないかもしれません。しかし、ZipEntry オブジェクトをそのまま ListBox のアイテムとして登録するだけで、書庫メンバ一覧として機能している事を考慮すべきです。
書庫のメンバ名だけでなく、更新日時とか、ファイルのオリジナルサイズ/圧縮サイズまで表示したりする場合は、ZipEntry のプロパティ等へのアクセスが必要な為、もっと面倒な事になります。

4.ZIP書庫の新規作成

書庫の新規作成は、FastZip クラスを使いました。また、イベントも使えるのですが、これも1つだけあえて使ってみました。

/// <summary>
/// 静的リンクによるフォルダ単位の新規書庫作成
/// </summary>
/// <param name="archiveFile">作成するZIP書庫のフルパス</param>
/// <param name="compFolder">書庫作成元フォルダパス</param>
private void CreateZip1(string archiveFile, string compFolder)
{
    ICSharpCode.SharpZipLib.Zip.FastZipEvents fastZipEvents =
        new ICSharpCode.SharpZipLib.Zip.FastZipEvents();
    fastZipEvents.Progress =
        new ICSharpCode.SharpZipLib.Core.ProgressHandler(CreateProgress);
    ICSharpCode.SharpZipLib.Zip.FastZip fastZip =
        new ICSharpCode.SharpZipLib.Zip.FastZip(fastZipEvents);
    fastZip.CreateEmptyDirectories = true;
    fastZip.UseZip64 = ICSharpCode.SharpZipLib.Zip.UseZip64.Dynamic;
    fastZip.CreateZip(archiveFile, compFolder, true, null);
}
// CreateZip1メソッド内から Progress イベント処理として呼び出される。
private void CreateProgress(Object sender, ICSharpCode.SharpZipLib.Core.ProgressEventArgs e)
{
    Console.WriteLine("{0} {1}%({2}byte/{3}byte)", e.Name, e.PercentComplete, e.Processed, e.Target);
}

/// <summary>
/// 動的リンクによるフォルダ単位の新規書庫作成
/// </summary>
/// <param name="archiveFile">作成するZIP書庫のフルパス</param>
/// <param name="compFolder">書庫作成元フォルダパス</param>
private void CreateZip2(string archiveFile, string compFolder)
{
    // FastZipEvent の Type 取得とインスタンス生成
    Type fastZipEvents = zip.GetType("ICSharpCode.SharpZipLib.Zip.FastZipEvents", true, false);
    object fze = fastZipEvents.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);

    // ProgressHandler の Type 取得と CreateProgress メソッドを関連付けて
    // ProgressHandler のインスタンス生成
    Type progressHandler = zip.GetType("ICSharpCode.SharpZipLib.Core.ProgressHandler", true, false);
    Type thisType = Assembly.GetExecutingAssembly().GetType("Acha_ya.SampleApplication.Form1");
    MethodInfo mi = thisType.GetMethod("CreateProgress",
        BindingFlags.NonPublic | BindingFlags.Instance);
    Delegate d = Delegate.CreateDelegate(progressHandler, this, mi);
    fastZipEvents.InvokeMember("Progress", BindingFlags.SetField, null, fze, new object[] { d });

    // FastZip の Type 取得とインスタンス生成&プロパティ設定
    Type fastZip = zip.GetType("ICSharpCode.SharpZipLib.Zip.FastZip", true, false);
    object fz = fastZip.InvokeMember(null, BindingFlags.CreateInstance, null, null,
        new object[] { fze });
    fastZip.InvokeMember("CreateEmptyDirectories", BindingFlags.SetProperty,
        null, fz, new object[] { true });
    Type useZip64 = zip.GetType("ICSharpCode.SharpZipLib.Zip.UseZip64", true, false);
    fastZip.InvokeMember("UseZip64", BindingFlags.SetProperty,
        null, fz, new object[] { useZip64.GetField("Dynamic").GetValue(fz) });

    // FastZip の CreateZip メソッド実行による書庫作成
    fastZip.InvokeMember("CreateZip", BindingFlags.InvokeMethod,
        null, fz, new object[] { archiveFile, compFolder, true, null });
}
// CreateZip2メソッド内から Progress イベント処理として呼び出される。
private void CreateProgress(Object sender, EventArgs e)
{
    Type progressEventArgs = zip.GetType("ICSharpCode.SharpZipLib.Core.ProgressEventArgs", true, false);
    Console.WriteLine("{0} {1}%({2}byte/{3}byte)",
        progressEventArgs.GetProperty("Name").GetValue(e, null),
        progressEventArgs.GetProperty("PercentComplete").GetValue(e, null),
        progressEventArgs.GetProperty("Processed").GetValue(e, null),
        progressEventArgs.GetProperty("Target").GetValue(e, null));
}

見ての通り、ここまで来るとかなり面倒にまります。これだと、動的リンク方式は、はっきりと生産性・保守性が失われる事が明白ですね。

5.動的リンクは実用に耐えられるか

SharpZipLib は、もちろん上記以外の機能もいろいろあり、最後にダウンロード出来るサンプルでは、一括展開処理(上書き確認付き)等も入れてあります。ですが、書庫の新規作成のコードを見ると、もう記事として出す必要もなさそうです。

本格的に処理を行なおうとした場合、単純に静的リンクされたコードを動的リンクで使えるコードに変換するのは現実的でない(少なくとも SharpZipLib の場合は非現実的)と思えますね。

では、動的リンクは諦めるのが正解でしょうか? 通常なら動的リンクしたいのには、それなりの理由がある訳ですから、静的リンクより面倒になっても、それが現実的に実現可能な範囲内に収まれば良いといえます。

そこで次回は、SharpZipLib (ICSharpCode.SharpZipLib.dll) をラップしたクラスを SharpZipLib の静的リンクで作成し、SharpZipLib 独自の型定義等を極力外部に出さない様にしてみましょう。そして、このラップした DLL を動的リンクして使用してみます。

では、最後に今回のサンプルコード+αのテストAP関連ソース一式を、CAB形式にしたファイルを置いておきます。

「DLLLink.cab」をダウンロード

« 処理時間の計測 | トップページ | マネージドDLLの動的リンク(2) »

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

コメント

コメントを書く

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

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

トラックバック


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

« 処理時間の計測 | トップページ | マネージドDLLの動的リンク(2) »