VB.NETでもDLLをアンロードしたい
はじめに
この記事はVisual Basic Advent Calendar 2015の6日目のエントリです。
今回はちょっとした理由で自前でプラグインっぽい何かの仕組みを構築する都合があったので、それについての記事です。
やりたい事
今回の仕組みの前提として大体以下のような感じです。
- プラグインをホストするアプリケーションは常駐する
- 必要な機能をプラグインとして実装して実行中に動的にロードする
- 1つの機能を1つのクラスとして表現し、機能を表すクラスは属性でマークする
- よって、ホスト側は実装時にはプラグインのクラス名は分からない
- 実行するクラスは属性から決定する
- ホストが実行中に機能をdllごと更新できるように使い終わったdllはアンロードしたい
- ホストからプラグインに情報を渡したい
環境
弊社での検証結果は以下の通りとなっています。
- Windows 8.1 Pro 64bit
- Visual Studio 2015
- .NET Framework 4.5.2
AppDomain
.NETではアセンブリ*1はAppDomain
ごとにロードされ、アンロードする場合はAppDomain
ごとアンロードする必要があります。
アセンブリ単独ではアンロード出来ません。
ほいほいアセンブリをアンロード出来たらプログラマのメンタルアセンブリをまたがるクラス間の依存関係が壊れてしまいますからね。
その為、アセンブリをアンロードするためにアプリケーションが実行されるドメインとプラグインが実行するドメインを分けて管理します。
アセンブリ構成
今回のプラグインっぽいアレでは、こんな感じにアセンブリを分割しました。
- UnloadablePlugin
- プラグインをホストするアプリケーション
- PluginMarker
- プラグインクラスが実装するインターフェースと属性
- PluginProxy
- Plugin
- プラグインの実装
各アセンブリ間の依存とアセンブリがロードされるドメインは以下のような感じです。
実装
アセンブリの依存順にコードを見てみましょう。
' PluginMarker.dll Public Interface IExecutablePlugin Function Execute(arg As String) As String End Interface <AttributeUsage(AttributeTargets.Class, AllowMultiple:=False)> Public Class PluginAttribute Inherits Attribute End Class
PluginMarker.dll
ではプラグインが実装すべきインターフェースとプラグインを表すクラスを示す属性が定義されています。
それだけです。
' PluginProxy.dll Imports System.Reflection Imports System.Text Imports PluginMarker Public Class Proxy Inherits MarshalByRefObject Private targets As IEnumerable(Of Type) Public Sub LoadDLL(dllAbsolutePath As String) Dim asm As Assembly = Assembly.LoadFile(dllAbsolutePath) targets = asm.GetTypes(). Where(Function(it) it.GetCustomAttribute(GetType(PluginAttribute)) IsNot Nothing) End Sub Public Function Execute(arg As String) As String Dim sb As StringBuilder = New StringBuilder() For Each it As Type In targets Dim i As IExecutablePlugin = CType(Activator.CreateInstance(it), IExecutablePlugin) sb.Append(i.Execute(arg)).AppendLine() Next Return sb.ToString() End Function End Class
PluginProxy.dll
はプラグインドメイン内でプラグインアセンブリからクラスを検索し実行する役割を担います。
まぁ、仰々しい事を書いていますが、コードを見てもらえば分かりますがかなり単純なコードとなっています。
大きな流れとしては
といった感じです。
また、注意点としてドメインの境界を越えるクラスはMarshalByRefObject
を継承するかSerializable
属性が付与されている(=シリアル化が可能)なクラスでなくてはなりません。
Proxy
クラスはMarshalByRefObject
を継承し、パラメータのString
はSerializable
属性が付与されているのでokですね。
' UnloadablePlugin.exe Imports PluginProxy Imports System.Reflection Imports System.IO Module Module1 Sub Main() Dim pluginDLLPath As String = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly.Location), "Assembly", "Plugin.dll") While (File.Exists(pluginDLLPath)) Dim pluginProxyPath As String = Path.Combine( Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "PluginProxy.dll") Dim pluginProxyAssm As Assembly = Assembly.LoadFile(pluginProxyPath) Dim pluginDomain As AppDomain = AppDomain.CreateDomain("PluginDomain") Dim pluginProxy As Proxy = CType(pluginDomain.CreateInstanceAndUnwrap( pluginProxyAssm.FullName, GetType(Proxy).FullName), Proxy) pluginProxy.LoadDLL(pluginDLLPath) Dim returnMessage As String = pluginProxy.Execute("あなたはだあれ?") Console.WriteLine(">>>") Console.WriteLine(returnMessage.Trim) Console.WriteLine("<<<") Console.WriteLine() AppDomain.Unload(pluginDomain) Threading.Thread.Sleep(5000) End While End Sub End Module
最後にUnloadablePlugin.exe
ですが、5秒ごとにあらかじめ決められたパスのdllが存在しているか確認を行い、存在したらProxy
経由でロードして実行を行います。
PluginProxy
アセンブリを読み込む- プラグイン用ドメインを作成する
- プラグイン用ドメイン内で
Proxy
インスタンスを生成する Proxy
経由でプラグインに値を渡し、受け取った値をコンソールに表示- プラグインをプロキシごとアンロード
って感じでやってます。
UnloadablePlugin.exe
(デフォルトドメイン)からはPlugin
を直接触っていないのでドメインにロードもされていません。
今回はプログラム中で固定でdllパスを参照していますが、特定のディレクトリをスキャンしてdllを読み込むという事も出来ますね。
'Plugin.dll Imports PluginMarker Imports System.Reflection <Plugin> Public Class Plugin1 Implements IExecutablePlugin Public Function Execute(arg As String) As String Implements IExecutablePlugin.Execute Console.WriteLine($"PluginClass={NameOf(Plugin1)}") Console.WriteLine($"arg={arg}") Console.WriteLine($"Domain={AppDomain.CurrentDomain.FriendlyName}") Console.WriteLine($"Assembly={Assembly.GetCallingAssembly.FullName}") Console.WriteLine() Return New String(arg.Reverse.ToArray) End Function End Class <Plugin> Public Class Plugin2 Implements IExecutablePlugin Public Function Execute(arg As String) As String Implements IExecutablePlugin.Execute Console.WriteLine($"PluginClass={NameOf(Plugin2)}") Console.WriteLine($"arg={arg}") Console.WriteLine($"Domain={AppDomain.CurrentDomain.FriendlyName}") Console.WriteLine($"Assembly={Assembly.GetCallingAssembly.FullName}") Console.WriteLine() Return arg + arg End Function End Class Public Class Plugin3 Implements IExecutablePlugin Public Function Execute(arg As String) As String Implements IExecutablePlugin.Execute Console.WriteLine($"PluginClass={NameOf(Plugin3)}") Console.WriteLine($"arg={arg}") Console.WriteLine($"Domain={AppDomain.CurrentDomain.FriendlyName}") Console.WriteLine($"Assembly={Assembly.GetCallingAssembly.FullName}") Return New String(arg.Reverse.ToArray) + New String(arg.Reverse.ToArray) End Function End Class
プラグインの方は特に意味はありません。 パラメータとして受け取った文字列を適当に加工して返しているだけです。
実行結果では、Plugin
属性を付与していないPlugin3
は実行されていません。
また、実行ドメインはPluginDomain
となっている事が分かります。
PluginClass=Plugin1 arg=あなたはだあれ? Domain=PluginDomain Assembly=PluginProxy, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null PluginClass=Plugin2 arg=あなたはだあれ? Domain=PluginDomain Assembly=PluginProxy, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null >>> ?れあだはたなあ あなたはだあれ?あなたはだあれ? <<<
リフレクションの代償
正直なところ、VBの主戦場たる生産性アプリケーションの領域ではまず使わない手法ですね。 まぁ、正直かなり面倒だしこの手法が必要になる場面が絶対的に少ないですしおすし。
また、動的に型を扱うということは本来ならコンパイラが面倒を見てくれる型の検証などを全て自分で行わなくてはならず、うっかりミスで実行時に死ぬなんてことがザラにあるのでその辺も気をつける必要があります。
_人人人人人人_ > 突然の死 <  ̄Y^Y^Y^Y^Y^Y^ ̄
また、Static(Shared)おじさんばりに都市伝説かもしれませんが、リフレクションを例外無しに全面的に禁止しているコーディング規則もあるらしいのでそういうところでは使えませんね。
おわりに
とりあえずリフレクションを使えるようにして失った感情の輝きを取り戻したいですね。いや、こっちのお話です。
まぁ、使うことはあまりないと思いますが、こういうことが出来るんだな〜程度に覚えておくといつか役に立つ日がくるといいな〜と願いつつVisual Basic Advent Calendar 2015の6日目のエントリとさせて頂きます。
おつかれさまでした。
*1:dllとかexeのアレ