【テクニカル・上級編】VB.NETで作る独自の簡易プラグインアーキテクチャ:Assembly.LoadFromを用いた外部DLL動的読み込みとインターフェイス分離による拡張性確保 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク
スポンサーリンク

密結合の呪縛を解け:VB.NETにおける動的プラグインアーキテクチャの極意

業務アプリケーションの現場において、エンジニアを最も苦しめるのは「仕様変更」だ。新しい帳票形式が追加されるたびに、本体のプロジェクトを開き、参照を書き換え、再ビルドし、巨大なバイナリを再配布する——。もし君がこのような「モノリス(巨大な単一構造)」の保守に明け暮れているとしたら、君はアーキテクチャの敗北を喫していると言わざるを得ない。

真に堅牢なシステムとは、本体(Host)が「何をするか」を知らずとも、「何ができるか」というインターフェイスの契約(Contract)のみに基づき、後から追加される部品(Plugin)を無慈悲に受け入れる構造を持つべきだ。

本稿では、`Assembly.LoadFrom` を用いた動的DLL読み込み、そしてメモリリークと参照の迷宮を回避するための、極限のプラグイン設計手法を伝授する。

1. 聖域の分離:インターフェイス・アセンブリの確立

プラグインアーキテクチャを構築する際、初心者が犯す最大の過ちは、本体プログラムとプラグインが直接お互いの型を参照してしまうことだ。これでは、プラグインを読み込むために本体を再ビルドしなければならず、本末転倒である。

解決策は、「契約(Interface)だけを定義した、極めて軽量な共有ライブラリ」を独立させることだ。

構成図

1. `Common.Contract.dll` (共有インターフェイス:これだけを本体とプラグインが参照する)
2. `MainApp.exe` (本体:プラグインの存在を知らず、`IPlugin` インターフェイスのみを扱う)
3. `Plugin_ReportA.dll` (プラグイン:`IPlugin` を実装した実体)

[Code] 共有インターフェイスの定義 (`Common.Contract`)

' Common.Contract プロジェクト
Public Interface IPlugin
    ''' <summary>
    ''' プラグインの識別名を取得する
    ''' </summary>
    ReadOnly Property PluginName As String

    ''' <summary>
    ''' メインの実行ロジック。引数にはコンテキスト(設定情報等)を渡す設計が望ましい
    ''' </summary>
    ''' <param name="inputData">処理対象のデータ</param>
    ''' <returns>実行結果のメッセージ</returns>
    Function Execute(inputData As Object) As String

    ''' <summary>
    ''' 解放処理。外部リソース(ファイルハンドル、DB接続等)を確実に閉じるための儀式
    ''' </summary>
    Sub Dispose()
End Interface

2. 動的ロードの実装:`Assembly.LoadFrom` の真実

本体プログラムは、特定のディレクトリ(例:`.\plugins\`)をスキャンし、見つかったDLLから `IPlugin` を実装したクラスを抽出する。ここで使用するのが `System.Reflection` だ。

[Code] プラグインローダーの実装 (`MainApp`)

Imports System.Reflection
Imports System.IO
Imports Common.Contract ' 共有インターフェイスのみを参照

Public Class PluginLoader
    ''' <summary>
    ''' 指定されたディレクトリからプラグインをロードし、リストとして返す
    ''' </summary>
    Public Function LoadPlugins(ByVal pluginDirectory As String) As List(Of IPlugin)
        Dim plugins As New List(Of IPlugin)()

        ' ディレクトリが存在しない場合は即座に脱出
        If Not Directory.Exists(pluginDirectory) Then Return plugins

        ' DLLファイルを走査
        Dim dllFiles = Directory.GetFiles(pluginDirectory, "*.dll")

        For Each dllPath In dllFiles
            Try
                ' Assembly.LoadFrom を使用してバイナリをメモリ空間へ展開
                Dim assembly As Assembly = Assembly.LoadFrom(dllPath)

                ' アセンブリ内の型を走査
                For Each t As Type In assembly.GetTypes()
                    ' IPlugin インターフェイスを実装しており、かつ抽象クラスではない型を探す
                    If GetType(IPlugin).IsAssignableFrom(t) AndAlso Not t.IsInterface AndAlso Not t.IsAbstract Then
                        ' インスタンス化
                        Dim pluginInstance As IPlugin = DirectCast(Activator.CreateInstance(t), IPlugin)
                        plugins.Add(pluginInstance)
                    End If
                Next
            Catch ex As Exception
                ' 実務ではここでログ出力。特定のDLLが壊れていても、他のプラグインのロードを止めないことが肝要
                Debug.WriteLine($"Failed to load {dllPath}: {ex.Message}")
            End Try
        Next

        Return plugins
    End Function
End Class

3. 伝説のエンジニアが教える「メモリ管理と生存期間」の罠

ここからが、シニアエンジニアが直面する「深淵」の話だ。

`Assembly.LoadFrom` でロードしたDLLは、一度メモリに展開されると、そのプロセスが終了するまで、あるいは「そのアセンブリをロードした AppDomain がアンロードされるまで」、メモリから消えることはない。

ゾンビ・オブジェクトの恐怖

もし、プラグインが大量のメモリを消費する計算ロジックを持っていた場合、プラグインを「使わなくなったからリストから外す」だけでは不十分だ。本体がそのプラグインのインスタンスを、あるいはそのプラグインが生成したオブジェクトへの参照を一つでも保持し続けている限り、GC(ガベージコレクション)は対象を回収できず、メモリリークを引き起こす。

対策:明示的な `Dispose` と 参照の遮断

1. `IDisposable` の徹底: `IPlugin` に `Dispose` を含め、プラグイン内部で保持している Windows API のハンドル(`IntPtr`)や、COMオブジェクト、ファイルストリームを確実に解放させる。
2. 参照の完全抹消: 本体側では、プラグインの使用が終わったら、リストから削除するだけでなく、変数を `Nothing` に設定し、参照の連鎖を断ち切る。

[Code] 安全なプラグイン実行サイクル

' 本体側の実行ルーチン
Public Sub RunPluginCycle()
    Dim loader As New PluginLoader()
    Dim activePlugins As List(Of IPlugin) = loader.LoadPlugins(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "plugins"))

    Try
        For Each plugin In activePlugins
            ' 実行
            Dim result = plugin.Execute("Sample Data")
            Console.WriteLine($"[{plugin.PluginName}] Result: {result}")
        Next
    Finally
        ' 【極めて重要】エラーが発生しても必ず解放処理を行う
        For Each plugin In activePlugins
            Try
                plugin.Dispose() ' プラグイン内部のリソース解放
            Catch ex As Exception
                ' Dispose中のエラーは致命的だが、ループを止めないよう捕捉
            End Try
        Next
        ' 参照をクリアしてGCの対象にする
        activePlugins.Clear()
        activePlugins = Nothing
    End Try
End Sub

4. レガシー環境と Windows API への洞察

古い業務システムでは、プラグインから Windows API(`user32.dll` や `kernel32.dll`)を叩いて、特定のウィンドウを操作したり、レジストリを書き換えたりするケースが多々ある。

ここで注意すべきは、「プラグインの例外が本体を道連れにする」リスクだ。
プラグイン内で `DllImport` を使用して不正なメモリアクセス(Access Violation)が発生した場合、CLR(Common Language Runtime)はプロセス全体を強制終了させる。

これを防ぐためには、プラグインの実装者に対し、以下の規律を課すべきだ:

  • 境界防御: プラグインの `Execute` メソッドの入り口と出口に、徹底的な `Try…Catch` を配置すること。
  • 外部リソースの管理: `SafeHandle` を使用し、Windows API のハンドル管理を自動化すること。

結論:アーキテクチャは「未来の自分」への贈り物である

`Assembly.LoadFrom` による動的ロードは、単なる技術的なテクニックではない。それは、「システムの進化を、本体の再コンパイルという苦行から解放する」という設計思想の表明である。

インターフェイスを厳格に定義し、メモリのライフサイクルを支配し、例外の伝播を制御する。この「規律」こそが、数年後にメンテナンスの地獄を見るか、あるいは優雅に新機能をデプロイできるかの分水嶺となる。

コードを書くときは常に自問せよ。
*「この変更のために、私は本体のバイナリを汚すべきか?」*

答えが「No」であれば、君は正しい道を歩んでいる。

タイトルとURLをコピーしました