.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
    ''' <summary>
    ''' プラグインに関する情報
    ''' </summary>
    Public Class PluginInfo
        Private _location As String
        Private _className As String
 
        ''' <summary>
        ''' PluginInfoクラスのコンストラクタ
        ''' </summary>
        ''' <param name="path">アセンブリファイルのパス</param>
        ''' <param name="cls">クラスの名前</param>
        Private Sub New(ByVal path As String, ByVal cls As String)
            Me._location = path
            Me._className = cls
        End Sub
 
        ''' <summary>
        ''' アセンブリファイルのパス
        ''' </summary>
        Public ReadOnly Property Location() As String
            Get
                Return _location
            End Get
        End Property
 
        ''' <summary>
        ''' クラスの名前
        ''' </summary>
        Public ReadOnly Property ClassName() As String
            Get
                Return _className
            End Get
        End Property
 
        ''' <summary>
        ''' 有効なプラグインを探す
        ''' </summary>
        ''' <returns>有効なプラグインのPluginInfo配列</returns>
        Public Shared Function FindPlugins() As PluginInfo()
            Dim plugins As New System.Collections.ArrayList
            'IPlugin型の名前
            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
 
            '.dllファイルを探す
            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
                            'PluginInfoをコレクションに追加する
                            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
 
        ''' <summary>
        ''' プラグインクラスのインスタンスを作成する
        ''' </summary>
        ''' <returns>プラグインクラスのインスタンス</returns>
        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
{
    /// <summary>
    /// プラグインに関する情報
    /// </summary>
    public class PluginInfo
    {
        private string _location;
        private string _className;
 
        /// <summary>
        /// PluginInfoクラスのコンストラクタ
        /// </summary>
        /// <param name="path">アセンブリファイルのパス</param>
        /// <param name="cls">クラスの名前</param>
        private PluginInfo(string path, string cls)
        {
            this.Location = path;
            this.ClassName = cls;
        }
 
        /// <summary>
        /// アセンブリファイルのパス
        /// </summary>
        public string Location
        {
            get {return _location;}
        }
 
        /// <summary>
        /// クラスの名前
        /// </summary>
        public string ClassName
        {
            get {return _className;}
        }
 
        /// <summary>
        /// 有効なプラグインを探す
        /// </summary>
        /// <returns>有効なプラグインのPluginInfo配列</returns>
        public static PluginInfo[] FindPlugins()
        {
            System.Collections.ArrayList plugins =
                new System.Collections.ArrayList();
            //IPlugin型の名前
            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 +
                    "\"が見つかりませんでした。");
 
            //.dllファイルを探す
            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)
                        {
                            //PluginInfoをコレクションに追加する
                            plugins.Add(
                                new PluginInfo(dll, t.FullName));
                        }
                    }
                }
                catch
                {
                }
            }
 
            //コレクションを配列にして返す
            return (PluginInfo[]) plugins.ToArray(typeof(PluginInfo));
        }
 
        /// <summary>
        /// プラグインクラスのインスタンスを作成する
        /// </summary>
        /// <returns>プラグインクラスのインスタンス</returns>
        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
        '/ <summary>
        '/ エントリポイント
        '/ </summary>
        <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()
 
            'プラグインのRunメソッドを呼び出して結果を取得する
            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
    {
        /// <summary>
        /// エントリポイント
        /// </summary>
        [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();
 
            //プラグインのRunメソッドを呼び出して結果を取得する
            string result = plugins[number].Run(str);
 
            //結果の表示
            Console.WriteLine("結果:");
            Console.WriteLine(result);
 
            Console.ReadLine();
        }
 
    }
}

メインアプリ実行前に実行ファイルのあるフォルダに"plugins"というフォルダを作り、そこに先ほど作成した"CountChars.dll"をコピーしておいてください。これで、"CountChars.dll"をプラグインとして認識できます。

基本的なプラグイン機能の実現方法は以上です。しかしより高度なプラグイン機能が必要な場合には、これだけでは不十分かもしれません。例えば、プラグインからメインアプリにフィードバックしたいなど、プラグインからメインアプリケーションの機能(メソッド)を呼び出したいケースもあるでしょう。そのような場合には、プラグインを使う側で実装するインターフェイスを定義するという方法があります(実は一番初めに紹介した「プラグイン機能の実現のために参考になりそうな記事」のすべてでこの方法が使われています)。

この方法については、次回、Windowsアプリケーションを例に紹介する予定です。

コメント



ページ情報
  • カテゴリ : .NET
  • 作成日 : 2004-08-16 (月) 06:00:00
  • 作成者 : DOBON!
  • 最終編集日 : 2010-03-21 (日) 02:38:47
  • 最終編集者 : DOBON!
[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]