.NETプログラミング研究 第39号 †
.NET Tips †
プラグイン機能を持つアプリケーションを作成する - その1 †
Adobe PhotoshopやBecky! Internet Mailなどのアプリケーションでは、「プラグイン」(または、「アドイン」、「エクステンション」等)と呼ばれるプログラムをインストールすることにより、機能を追加することができるようになっています。ここでは、このようなプラグイン機能を持ったアプリケーションの作り方を考えます。
(プラグインが何だか分からないという方は、「アスキー デジタル用語辞典」や「IT用語辞典 e-Words」等をご覧ください。)
早速ですが、プラグイン機能の実現のために参考になりそうな記事を以下にいくつか紹介します。
これらの記事で紹介されている方法は基本的にはほとんど同じで、それは、インターフェイスを使用するという方法です。つまり、プラグインとして必要となる機能をプロパティやメソッド(さらに、イベントやインデクサ)として持つインターフェイスをあらかじめ用意しておき、このインターフェイスを実装したクラスとしてプラグインを作成するのです。
インターフェイスの作成 †
理屈は置いておき、実際にプラグインを作成してみましょう。
ここで作成するプラグインは、プラグインの名前を返す機能と、渡された文字列を処理して、結果を文字列として返す機能を有するものとし、それぞれの機能のためにNameプロパティとRunメソッドを持つインターフェイスを作ります。
Visual Studio .NETでは、クラスライブラリのプロジェクトを作成し、次のようなコードを書き、ビルドします。.NET SDKの場合は、/target:libraryコンパイラオプションにより、コードライブラリを作成します。なお、ここで作成されたアセンブリファイル名は、"Plugin.dll"であるとします。
(Visual Studio .NETのVB.NETの場合は、プロジェクトのプロパティの「ルート名前空間」が空白になっているものとします。デフォルトではプロジェクト名となっています。)
1
2
3
4
5
6
7
8
| | Imports System
Namespace Plugin
Public Interface IPlugin
ReadOnly Property Name() As String
Function Run(ByVal str As String) As String
End Interface
End Namespace
|
1
2
3
4
5
6
7
8
9
10
| | using System;
namespace Plugin
{
public interface IPlugin
{
string Name {get;}
string Run(string str);
}
}
|
プラグインの作成 †
次に、このインターフェイスを基に、プラグインを作ります。プラグインもクラスライブラリとして作成します。プラグインは今作成したIPluginインターフェイスを実装する必要がありますので、"Plugin.dll"を参照に追加します。具体的には、Visual Studio .NETの場合は、ソリューションエクスプローラの「参照設定」に"Plugin.dll"を追加します。.NET SDKの場合は、/referenceコンパイラオプションを使用します。
ここでは渡された文字列の文字数を返すプラグインを作成することにします。IPluginインターフェイスを実装した次のようなCountCharsクラスを作成します。このクラスライブラリは"CountChars.dll"という名前のアセンブリファイルとしてビルドするものとします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| | Imports System
Namespace CountChars
Public Class CountChars
Implements Plugin.IPlugin
Public ReadOnly Property Name1() As String _
Implements Plugin.IPlugin.Name
Get
Return "文字数取得"
End Get
End Property
Public Function Run1(ByVal str As String) As String _
Implements Plugin.IPlugin.Run
Return str.Length.ToString()
End Function
End Class
End Namespace
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| | using System;
namespace CountChars
{
public class CountChars : Plugin.IPlugin
{
public string Name
{
get
{
return "文字数取得";
}
}
public string Run(string str)
{
return str.Length.ToString();
}
}
}
|
プラグインを使うアプリケーションの作成 †
残るはプラグインを使用するメインアプリケーションの作成です。メインアプリケーションでは、インストールされている有効なプラグインをどのように探すか、そして、プラグインクラスのインスタンスをどのように作成するかということが問題となります。
まずインストールされているプラグインを探す方法についてです。プラグインは必ず".dll"という拡張子を持ち、指定されたフォルダに置かれていなければならないという約束にしておきます(ここでは、メインアプリケーションがインストールされているフォルダにある"plugins"フォルダにプラグインを置くものとします)。これにより、メインアプリケーションはプラグインフォルダにある".dll"の拡張子を持つファイルのみを調べればよいことになります。さらに、アセンブリにIPluginインターフェイスを実装したクラスがあるか調べるには、リフレクションを使用します。
またプラグインクラスのインスタンスを作成するには、Activator.CreateInstanceメソッドなどを使用すればよいでしょう(下のサンプルでは、Assembly.CreateInstanceメソッドを使用しています)。
それでは実際に作成してみましょう。ここではメインアプリはコンソールアプリケーションとして作成します。また、"Plugin.dll"を参照に追加します。
まずはプラグインを扱うクラス("PluginInfo")を作成します。このクラスは、プラグインのアセンブリファイルのパスとクラス名を返すプロパティ(それぞれ"Location"と"ClassName")と、有効なプラグインを探すメソッド("FindPlugins")、IPluginのインスタンスを作成するメソッド("CreateInstance")を持ちます。このクラスのコードを以下に示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
| | Imports System
Namespace MainApplication
Public Class PluginInfo
Private _location As String
Private _className As String
Private Sub New(ByVal path As String, ByVal cls As String)
Me._location = path
Me._className = cls
End Sub
Public ReadOnly Property Location() As String
Get
Return _location
End Get
End Property
Public ReadOnly Property ClassName() As String
Get
Return _className
End Get
End Property
Public Shared Function FindPlugins() As PluginInfo()
Dim plugins As New System.Collections.ArrayList
Dim ipluginName As String = _
GetType(Plugin.IPlugin).FullName
Dim folder As String = _
System.IO.Path.GetDirectoryName( _
System.Reflection.Assembly. _
GetExecutingAssembly().Location)
folder += "\plugins"
If Not System.IO.Directory.Exists(folder) Then
Throw New ApplicationException( _
"プラグインフォルダ""" + folder + _
"""が見つかりませんでした。")
End If
Dim dlls As String() = _
System.IO.Directory.GetFiles(folder, "*.dll")
Dim dll As String
For Each dll In dlls
Try
Dim asm As System.Reflection.Assembly = _
System.Reflection.Assembly.LoadFrom(dll)
Dim t As Type
For Each t In asm.GetTypes()
If t.IsClass AndAlso t.IsPublic AndAlso _
Not t.IsAbstract AndAlso _
Not (t.GetInterface(ipluginName) _
Is Nothing) Then
plugins.Add( _
New PluginInfo(dll, t.FullName))
End If
Next t
Catch
End Try
Next dll
Return CType(plugins.ToArray( _
GetType(PluginInfo)), PluginInfo())
End Function
Public Function CreateInstance() As Plugin.IPlugin
Try
Dim asm As System.Reflection.Assembly = _
System.Reflection.Assembly.LoadFrom(Me.Location)
Return CType(asm.CreateInstance(Me.ClassName), _
Plugin.IPlugin)
Catch
End Try
End Function
End Class
End Namespace
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
| | using System;
namespace Plugin
{
public class PluginInfo
{
private string _location;
private string _className;
private PluginInfo(string path, string cls)
{
this.Location = path;
this.ClassName = cls;
}
public string Location
{
get {return _location;}
}
public string ClassName
{
get {return _className;}
}
public static PluginInfo[] FindPlugins()
{
System.Collections.ArrayList plugins =
new System.Collections.ArrayList();
string ipluginName = typeof(Plugin.IPlugin).FullName;
string folder = System.IO.Path.GetDirectoryName(
System.Reflection.Assembly
.GetExecutingAssembly().Location);
folder += "\\plugins";
if (!System.IO.Directory.Exists(folder))
throw new ApplicationException(
"プラグインフォルダ\"" + folder +
"\"が見つかりませんでした。");
string[] dlls =
System.IO.Directory.GetFiles(folder, "*.dll");
foreach (string dll in dlls)
{
try
{
System.Reflection.Assembly asm =
System.Reflection.Assembly.LoadFrom(dll);
foreach (Type t in asm.GetTypes())
{
if (t.IsClass && t.IsPublic && !t.IsAbstract &&
t.GetInterface(ipluginName) != null)
{
plugins.Add(
new PluginInfo(dll, t.FullName));
}
}
}
catch
{
}
}
return (PluginInfo[]) plugins.ToArray(typeof(PluginInfo));
}
public Plugin.IPlugin CreateInstance()
{
try
{
System.Reflection.Assembly asm =
System.Reflection.Assembly.LoadFrom(this.Location);
return (Plugin.IPlugin)
asm.CreateInstance(this.ClassName);
}
catch
{
return null;
}
}
}
}
|
FindPluginsメソッドが多少複雑ですので、簡単に説明しておきます。FindPluginsメソッドでは、Assembly.LoadFromメソッドでアセンブリを読み込んだ後、アセンブリ内のすべての型について、その型がクラスであり、パブリックであり、抽象クラスでないことを確認し、さらにType.GetInterfaceメソッドにより、IPluginインターフェイスを実装していることを確認します。これらすべての条件に当てはまった場合に有効なプラグインと判断します。(よってこの例では、1つのアセンブリファイルに複数のプラグインクラスを定義することができます。)
ここまで来たら、後は簡単です。PluginInfoクラスを使ったメインのクラス(エントリポイント)のサンプルは、次のようになります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| | Imports System
Namespace MainApplication
Public Class PluginHost
<STAThread()> _
Public Shared Sub Main()
Dim pis As PluginInfo() = PluginInfo.FindPlugins()
Dim plugins(pis.Length - 1) As Plugin.IPlugin
Dim i As Integer
For i = 0 To plugins.Length - 1
plugins(i) = pis(i).CreateInstance()
Next i
Dim number As Integer = -1
Do
For i = 0 To plugins.Length - 1
Console.WriteLine("{0}:{1}", i, plugins(i).Name)
Next i
Console.WriteLine( _
"使用するプラグインの番号を入力してください。:")
Try
number = Integer.Parse(Console.ReadLine())
Catch
End Try
Loop While number < 0 Or number >= pis.Length
Console.WriteLine(plugins(number).Name + " を使用します。")
Console.WriteLine("文字列を入力してください。:")
Dim str As String = Console.ReadLine()
Dim result As String = plugins(number).Run(str)
Console.WriteLine("結果:")
Console.WriteLine(result)
Console.ReadLine()
End Sub
End Class
End Namespace
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
| | using System;
namespace MainApplication
{
public class PluginHost
{
[STAThread]
static void Main(string[] args)
{
PluginInfo[] pis = Plugin.PluginInfo.FindPlugins();
Plugin.IPlugin[] plugins = new Plugin.IPlugin[pis.Length];
for (int i = 0; i < plugins.Length; i++)
plugins[i] = pis[i].CreateInstance();
int number = -1;
do
{
for (int i = 0; i < plugins.Length; i++)
Console.WriteLine("{0}:{1}", i, plugins[i].Name);
Console.WriteLine(
"使用するプラグインの番号を入力してください。:");
try
{
number = int.Parse(Console.ReadLine());
}
catch
{
Console.WriteLine("数字を入力してください。");
}
} while (number < 0 || number >= pis.Length);
Console.WriteLine(plugins[number].Name + " を使用します。");
Console.WriteLine("文字列を入力してください。:");
string str = Console.ReadLine();
string result = plugins[number].Run(str);
Console.WriteLine("結果:");
Console.WriteLine(result);
Console.ReadLine();
}
}
}
|
メインアプリ実行前に実行ファイルのあるフォルダに"plugins"というフォルダを作り、そこに先ほど作成した"CountChars.dll"をコピーしておいてください。これで、"CountChars.dll"をプラグインとして認識できます。
基本的なプラグイン機能の実現方法は以上です。しかしより高度なプラグイン機能が必要な場合には、これだけでは不十分かもしれません。例えば、プラグインからメインアプリにフィードバックしたいなど、プラグインからメインアプリケーションの機能(メソッド)を呼び出したいケースもあるでしょう。そのような場合には、プラグインを使う側で実装するインターフェイスを定義するという方法があります(実は一番初めに紹介した「プラグイン機能の実現のために参考になりそうな記事」のすべてでこの方法が使われています)。
この方法については、次回、Windowsアプリケーションを例に紹介する予定です。
コメント †