##概要
C#のアプリケーションで、ビルドの時点では未知なDLLをロードし、運用する方法について述べます。プラグインシステムの構築に役立つと思います。ついでにC#でのアセンブリの概念や、リフレクションの初歩にも触れます。
##アセンブリとは
C#でいうアセンブリとは、ざっくり言うとexeかdllのことです。実行可能なアプリケーションがexe、ライブラリがdllとなります。C#ではすべてのライブラリがdllとしてビルドされます。exeが実行可能であること以外は、両者にほとんど差はなく、ひっくるめてアセンブリと呼ばれることが多いです。
##静的な参照
C++でdllを利用する際には、インポートライブラリをリンクして参照を埋め込む方法と、LoadLibrary()やCOMなどを使って動的にdllをロードする方法がありました。
C#のアセンブリ参照にも、同じく2通りの方法があります。まずは静的な参照解決の方法をおさらいしておきます。
###プロジェクトに対して参照設定を行う
ソリューション内にライブラリのプロジェクトを追加し、
上記のメニューから「参照の追加」を選んで、参照マネージャを開きます。ここで「ソリューション→プロジェクト」内のライブラリプロジェクトをチェックすれば、もうそれだけで参照プロジェクト内のクラスが利用できます。
名前空間についてはフルパスで指定するか、usingを適宜利用してください。ClassLibrary_staticプロジェクト内のHogeStaticクラスを利用するなら、次のようになります。Messageプロパティの内容を、テキストボックスに表示させるコードです。
namespace ClassLibrary_static
{
public class HogeStatic
{
public string Message
{
get
{
return "私はHogeStaticである。";
}
}
}
}
var hoge = new ClassLibrary_static.HogeStatic();
this.textBox1.Text = hoge.Message;
##動的な参照
さて、本題です。静的な参照では当然のことながら、ビルド時に参照しているアセンブリしか利用できません。これではプラグイン的なことができないので、「アセンブリのロード」と「クラスインスタンスの実体化」の2段階に分けて手順を説明します。
###アセンブリのロード
拍子抜けするほど簡単です。System.Reflection名前空間にあるAssembly.LoadFrom()メソッドを使います。
var asm = Assembly.LoadFrom(dllPath);
引数にはdllファイルのフルパスを指定します。
###クラスインスタンスの生成
こちらも簡単なのですが、何せ動的にロードしたものなのでクラスの定義が一切ありません。なので、文字列ベースで名前を指定します。
var typeInfo = asm.GetType("ClassLibrary_dynamic.HogeDynamic");
dynamic hoge = Activator.CreateInstance(typeInfo);
this.textBox2.Text = hoge.Message;
まず、アセンブリオブジェクトから型情報を取り出します。GetType()メソッドに名前空間からのフルネームを渡すと、Type型のオブジェクトが取れます。ここからその型のインスタンスを作るには、Activator.CreateInstance()メソッドを使います。
(HogeDynamicはHogeStaticとほぼ一緒なので割愛します。)
しかし、CreateInstance()の返り値はobject型なので、このままではラチがあきません。Type型オブジェクトからメンバーへのアクセスを取得することもできますが、相当面倒です。なので、dynamicオブジェクトとしてインスタンスを受け取ります。
dynamicで宣言したオブジェクトは、ピリオドに続けて記述した名前は実行時に解決しようとします。上記のコードはすんなりビルドが通りますが実際には、
- おう、hogeインスタンスにMessageってメンバーがあるらしい。調べるぞ。
- メソッドかな?プロパティかな?お、プロパティだった。じゃあアクセス経路をつないで、っと
- 次からは顔パスでええからな~
といったやり取りが展開されています。"Message"という文字列をキーにして、Type型オブジェクトに問い合わせているわけですね。実はここらへんの処理は結構重かったりします。それでも一度調べがついたものをキャッシュしてくれるぶん、dynamicは優秀です。C#4.0より前は、ここらへんをスマートにやるのが相当面倒でした。
しかしこれ、お気付きかと思いますが実は結構危うい処理です。所詮は文字列ベースなので、タイプミスしたらハイそれまでよです。コンパイルエラーにはなりません。静的な定義がないためインテリセンスも効かないので、予期せぬ実行時エラーを招きやすくなります。
##インタフェースによる静的な名前の解決
プラグインのようなものを受け入れるのであれば、最低限の共通APIは規定すると思います。なので、
- すべてのプラグインは共通のインタフェースを実装する
- dynamicにするのではなく、生成したインスタンスをインタフェース型でキャストする
とすれば、インタフェースに定義されているメンバーに関しては、静的参照の時と同じように扱うことができます。インタフェースはプラグインと、本体アプリの両方から参照するので、もう一つプラグインマネージャ的なアセンブリを作ります。
(でないと静的・動的とはいえ、循環参照になってしまします。)
###インタフェースの定義
というわけで、プラグインマネージャ的なアセンブリにはこんなインタフェースを定義しておきます。
namespace PlugInManager
{
public interface InterfaceHoge
{
string Message { get; }
}
}
このプラグインマネージャを、プラグインと本体アプリのプロジェクトから静的に参照しておきます。これを継承して、
namespace ClassLibrary_dynamic
{
public class HogeImpl : PlugInManager.InterfaceHoge
{
public string Message
{
get
{
return "私はインタフェースを継承したHogeである。";
}
}
}
}
こんな感じに実装することにしましょう。
###文字列に頼らないクラスインスタンスの生成
InterfaceHoge hoge = null;
var asm = Assembly.LoadFrom(dllPath);
foreach (var t in asm.GetTypes())
{
if (t.IsInterface) continue;
hoge = Activator.CreateInstance(t) as InterfaceHoge;
if (hoge != null)
{
break;
}
}
if (hoge != null)
{
this.textBox3.Text = hoge.Message;
}
else
{
this.textBox3.Text = "<プラグインではないDLLを指定しました>";
}
というわけで、文字列ベースの処理を排除してみました。アセンブリからはGetTypes()で、そのアセンブリに収録されているpublicな型をまとめて取得できます。それをforeachで一つずつ調べて、インタフェースの型にキャストできればOKということでbreak、そうでなければ次の型を調べる……として、文字列を使わずにプラグインの実装クラスを探し出すことができます。asでキャストして、nullじゃなかったら処理するのはC#の定番パターンなので、覚えておきましょう。
ただし、それだけだとインタフェースそのものを取り出した場合にインタフェースを実体化しようとしてエラーになるケースが出てきます。なので、TypeクラスのIsInterfaceプロパティで、インタフェースだったら処理をスキップするようにしておけばいいでしょう。Typeクラスには他にも様々な情報が取得できるプロパティがあるので、もう少し厳密なチェックをした方がいいかもしれません。ぜひリファレンスをチェックしてみてください。
→http://msdn.microsoft.com/ja-jp/library/system.type(v=vs.110).aspx