2016年9月9日金曜日

C#で動的プラグインの作成(InterFace未使用版)

C#で動的プラグインを作るといえば、通常は、インターフェースを作り、インターフェース派生のプラグインを作って呼び出すのが普通だと思います。(そのうち書いてみます)

今日は、プラグイン側にもっと自由を与えてみたいと思います。
プラグインには、あの大空に翼を広げ飛んで行ってもらいます。(でも見えるところにいてくれないと困るけど)

呼び出すクラスと、呼び出す関数は、アプリケーション側では制御せず、プラグインを作成するタイミングで自由に決めることができます。

使った機能
  • カスタム属性
  • Activator
  • Invoke

構成としては、
  • アプリケーション(exe本体)
  • プラグイン識別用の属性定義(DLL)
  • プラグイン(DLL)

の3部作です。アプリケーションとプラグインは、属性定義(DLL)を参照しています。



では、さっそくサンプルコードです。

アプリケーション(exe)の実装

アプリケーション本体としては、次のような処理をしているだけです。
  1. 任意のフォルダーからdllを検索
  2. アセンブリ属性の確認(期待する属性の定義があれば、プラグインとみなす)
  3. タイプの取得(期待する属性の定義があれば、プラグインクラスとみなす)
  4. メソッドを呼び出す。
using PlugInAttribute;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
namespace ConsoleApp
{
//-------------------------------------------------------
/// <summary>
/// メソッド情報保持
/// </summary>
//-------------------------------------------------------
public class PlugInMethod
{
public Type ExeType { get; set; }
public IList<String> MethodNames { get; set; }
public PlugInMethod( Type type, IList<String> methods )
{
this.ExeType = type;
this.MethodNames = methods;
}
}
//-------------------------------------------------------
/// <summary>
/// 実行クラス
/// </summary>
//-------------------------------------------------------
class Program
{
static void Main(string[] args)
{
var myAsm = Assembly.GetEntryAssembly();
var exePath = new Uri(myAsm.Location).AbsolutePath;
var exeDir = Path.GetDirectoryName(exePath);
var methods = GetExecuteMethods(
exeDir, "dll",
SearchOption.TopDirectoryOnly);
// プラグインのメソッド呼び出し
foreach( var method in methods )
{
CallMethod( method );
}
}
//-------------------------------------------------------
/// <summary>
/// 実行メソッド一覧取得
/// </summary>
//-------------------------------------------------------
private static IList<PlugInMethod> GetExecuteMethods(
String dir, String extension, SearchOption seachOption )
{
var methods = new SortedList<Int32,PlugInMethod>();
var dllPaths = Directory.GetFiles(dir,"*."+extension,
seachOption);
// 対象dll検索
foreach( var dllPath in dllPaths )
{
var asmDll = GetPlugInAssembly(dllPath);
if( asmDll != null )
{
// 対象クラス検索
var classTypes = asmDll.GetTypes();
foreach( var classType in classTypes )
{
var typeAttrs = classType.GetCustomAttributes(
typeof(PlugInClassAttribute));
if( typeAttrs == null ||
typeAttrs.Count() == 0 )
{
continue;
}
var names = new SortedList<Int32,String>();
var classIndex = 0;
foreach( var typeAttr in typeAttrs )
{
var plugInClass = typeAttr as PlugInClassAttribute;
if( plugInClass != null )
{
classIndex = plugInClass.ClassIndex;
names.Add( plugInClass.MethodIndex,
plugInClass.ExecuteName );
}
}
// メソッド名を保持
methods.Add(
classIndex,
new PlugInMethod(
classType,
names.Values ) );
}
}
}
return methods.Values;
}
//-------------------------------------------------------
/// <summary>
/// PlugIn取得
/// </summary>
//-------------------------------------------------------
private static Assembly GetPlugInAssembly( String dllPath )
{
var asmDll = Assembly.LoadFile(dllPath);
if( asmDll != null )
{
var asmAttr = asmDll.GetCustomAttribute(
typeof(PlugInAssemblyAttribute));
if( asmAttr != null )
{
var asmPlugIn = asmAttr as PlugInAssemblyAttribute;
if( asmPlugIn != null )
{
return asmDll;
}
}
}
return null;
}
//-------------------------------------------------------
/// <summary>
/// メソッド呼び出し
/// </summary>
//-------------------------------------------------------
private static void CallMethod( PlugInMethod method )
{
var classType = method.ExeType;
var pluginInstance = Activator.CreateInstance(classType);
foreach( var methodName in method.MethodNames )
{
MethodInfo methodInfo = classType.GetMethod(methodName);
methodInfo.Invoke( pluginInstance, null );
}
}
}
}
view raw ConsoleApp.cs hosted with ❤ by GitHub

プラグイン用属性定義(DLL) 

属性定義では、次の2つのカスタム属性を定義しています。
  • アセンブリ用のカスタム属性
  • クラス用のカスタム属性
※クラス用のカスタム属性では、呼び出すクラスの順番、クラス内で呼び出す関数の順番を
指定できるようにしています。

using System;
namespace PlugInAttribute
{
//-------------------------------------------------------
/// <summary>
/// Assemblyがプラグインかどうかの判定をするための属性
/// </summary>
//-------------------------------------------------------
[System.AttributeUsage(System.AttributeTargets.Assembly)]
public class PlugInAssemblyAttribute : Attribute
{
public PlugInAssemblyAttribute() : base()
{
}
}
//-------------------------------------------------------
/// <summary>
/// クラスがプラグインのクラスかどうかの判定をするための属性
/// </summary>
//-------------------------------------------------------
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true )]
public class PlugInClassAttribute : Attribute
{
public String ExecuteName { get; set; }
public Int32 ClassIndex { get; set; }
public Int32 MethodIndex { get; set; }
public PlugInClassAttribute(
String executeName,
Int32 classIndex, Int32 methodIndex):base()
{
ExecuteName = executeName;
ClassIndex = classIndex;
MethodIndex = methodIndex;
}
}
}
view raw pluginAttr.cs hosted with ❤ by GitHub


プラグイン(DLL)

プラグインでは、アセンブリのカスタム属性と、各クラスで、クラスのカスタム属性を記載しています。
テストのために、属性の定義をしていないクラス(Sample3)も記載しています。

using System;
//このDllをプラグインとして認識させる
[assembly: PlugInAttribute.PlugInAssemblyAttribute]
namespace SamplePlugIn
{
/// <summary>
/// プラグインクラス1
/// </summary>
[PlugInAttribute.PlugInClass("ExecSampleA",2,1)]
[PlugInAttribute.PlugInClass("ExecSampleB",2,2)]
public class PlugInSample
{
public PlugInSample(){}
public void ExecSampleB()
{
Console.WriteLine( "Called SampleB" );
}
public void ExecSampleA()
{
Console.WriteLine("Called SampleA");
}
}
/// <summary>
/// プラグインクラス2
/// </summary>
[PlugInAttribute.PlugInClass("ExecSample2",1,1)]
public class Sample2
{
public Sample2() { }
public void ExecSample2()
{
Console.WriteLine("Called Sample2");
}
}
/// <summary>
/// 普通のクラス
/// </summary>
public class Sample3
{
public Sample3() { }
public void ExecSample3()
{
Console.WriteLine("Called Sample3");
}
}
}
view raw pluginSample.cs hosted with ❤ by GitHub


実行結果

実行結果は次のようになり、大成功でした。


Called Sample2
Called SampleA
Called SampleB


通常であれば、
ここまで、プラグインに自由を与えることはないと思います。
動的プラグインを作る際には、程よく使用していただければと思います。
(プラグインかどうかの判断で、アセンブリのカスタム属性くらいなら使うかも)



0 件のコメント:

コメントを投稿