C#でプラグイン風の機能を使ってみたのでメモ。
基本的なポイントは次の通り。
- 所定のディレクトリにあるプラグインを読み込む
- プラグイン自体は.NET FrameworkのDLL
- プラグインDLLは共通のインターフェイスをもつ
サンプルプロジェクトの構成
Visual Studio のソリューションにプロジェクトを4つ作成。
| プロジェクト名 | 種類 | 参照設定 | 備考 |
|---|---|---|---|
| HogeMain | Windows フォームアプリケーション | HogePlugin,Namihei,Fune | スタートアッププロジェクト |
| HogePlugin | クラスライブラリ | プラグインのインターフェイス | |
| Namihei | クラスライブラリ | HogePlugin | プラグイン1 |
| Fune | クラスライブラリ | HogePlugin | プラグイン2 |
プラグインの共通仕様をDLLにして共有する(HogePlugin)。中身は普通のインターフェイス。
HogeMain でプラグイン(Hamihei,Fune)を参照設定しているのは、実行フォルダにプラグインDLLを作成する為だけの目的なので本来は必要ない。
HogeMain にテキストボックスを1つ追加すれば準備完了。
HogePlugin.dll
まずはインターフェイスを共有する為のDLL。
namespace HogePlugin
{
public interface IHogePlugin
{
string Name { get; } // 名前
string GetComment(); // コメントを取得
}
}
プラグイン固有の名前(プロパティ)とコメントを返すメソッドを準備。
とりあえずサンプルなのでこれだけw
Namihei.dll
次に実際のプラグインの中身。
using HogePlugin;
namespace Namihei
{
public class Namihei : IHogePlugin
{
public string Name
{
get { return "波平"; }
}
public string GetComment()
{
return "バッカモーーン!";
}
}
}
上で作成した IHogePlugin インターフェイスを実装。
名前を「波平」として、コメント「バッカモーーン!」を返す昭和のお父さんプラグイン。
Fune.dll
もう一つプラグインを作成してみる。
using HogePlugin;
namespace Fune
{
public class Fune : IHogePlugin
{
public string Name
{
get { return "フネ"; }
}
public string GetComment()
{
return "あらあら。";
}
}
}
同じく IHogePlugin インターフェイスを実装。
名前が「フネ」、コメント「あらあら。」を返す昭和のお母さんプラグイン。
HogeMain.exe
最後にアプリケーション本体。
using HogePlugin;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
namespace HogeMain
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
// ディレクトリ内のプラグインを取得して配列で返す
private IHogePlugin[] GetPlugins(string path)
{
List<IHogePlugin> plugins = new List<IHogePlugin>();
// ディレクトリ内のDLLファイルパスを取得
foreach (string dll in Directory.GetFiles(path, "*.dll"))
{
// ファイルパスからアセンブリを読み込む
Assembly asm = Assembly.LoadFrom(dll);
// アセンブリで定義されている型を取得(例 Namiheiクラス)
foreach(Type type in asm.GetTypes())
{
// 非クラス型、非パブリック型、抽象クラスはスキップ
if (!type.IsClass || !type.IsPublic || type.IsAbstract) continue;
// 型に実装されているインターフェイスから IHogePlugin を取得
Type t = type.GetInterfaces().FirstOrDefault((_t) => _t == typeof(IHogePlugin));
// default(IHogePlugin) と等しい場合は未実装なのでスキップ
if (t == default(IHogePlugin)) continue;
// 取得した型のインスタンスを作成(例 Namiheiクラス)
object obj = Activator.CreateInstance(type);
plugins.Add((IHogePlugin)obj);
}
}
return plugins.ToArray();
}
private void button1_Click(object sender, EventArgs e)
{
// 実行ファイルのディレクトリを取得
string path = Path.GetDirectoryName(Application.ExecutablePath);
// ディレクトリ内のプラグインを取得
foreach (IHogePlugin plugin in this.GetPlugins(path))
{
// 名前とコメントを表示
MessageBox.Show(string.Format("{0}:{1}", plugin.Name, plugin.GetComment()));
}
}
}
}
FirstOrDefault は先頭の要素を返すメソッドだが、見つからない場合はデフォルトを返す。
なので default(IHogePlugin) とイコールであれば未実装と判断してスキップ。(この場合はnull)
if (t == default(IHogePlugin)) continue;
ここで IHogePlugin 型にキャストすれば
IHogePlugin plugin = (IHogePlugin)obj;
いつも通りクラスのインスタンスを作成した場合と同じように使える。
IHogePlugin plugin = new Namihei.Namihei();
まとめ
基本的な流れは次の通り。
- DLLファイルを探す
- ファイル名からアセンブリをロード
- アセンブリ内の型からプラグイン用インターフェイスを実装するクラスを探す
- クラスのインスタンスを作成
動的にDLLを読み込むのはさほど難しくはない。
むしろ実際はプラグイン自体の設計の方が大事・・・
