Top > Programming > .NetFramework > Tips > Using CSharp as Script
Last-modified: Mon, 05 May 2014 09:10:41 JST
Counter:6081 Today:1 Yesterday:1 Online:9
このエントリーをはてなブックマークに追加

C# をスクリプト言語にしてプラグインを実装する

ここでは、C# をスクリプト言語として扱い、C#(.net) アプリケーションの上で動作させる最も簡単な方法について解説します。例として、スクリプトをプラグインのように動作させるコンソールアプリケーションを実装しています。

コンソールアプリケーションは、(1)実行前に特定のディレクトリに含まれるスクリプトのソースコードを読み込み、(2)実行時にコンパイルして、(3)プラグインとして実行する、という処理の流れになっています。したがって、スクリプトを実行中に書き換えると、その書き換えが実行中のアプリケーションに反映されることになります。

補足

プラグインを実行時にコンパイルするとどうしても処理時間がかかります。現実的に、.Netのアプリケーションとしてプラグインの機能を実装する場合には、実行前に(プリ)コンパイルされた dll などを読み込むことになるでしょう。ここではその方法については解説しません。

プラグインファイルの用意

プラグインの機能を実装するためには、プラグインの仕様を決定する必要があります。ここではプラグインとして用意されたクラスは1つの Action メソッドを持つようにします。 Action メソッドは1つの int 型の引数を必要とします。また、プラグインファイルの名前はプラグインのクラス名と同じとして、プラグインはすべて plugins ディレクトリに入れておくものとします。

public class Punch
{
    public static int Action(int count)
    {
        for (int i = 0; i < count; i++ )
            Console.WriteLine("Punch");
        return 0;
    }
}

プラグインファイルの読み込み

まずはプラグインファイルを読み込みます。読み込み自体はそれほど難しいこともないので詳細な解説は割愛します。

Dictionary (≒ Hash) にプラグイン名とそのプラグインファイルのパスを保存します。ここではプラグイン名はプラグインファイルのファイル名とします。プラグインが保存されたディレクトリを指定して、そのディレクトリに含まれるすべての ".cs" ファイルを対象に、プラグイン名とプラグインファイルのパスを取得して Dictionary に保存します。

Dictionary<string, string> pluginDictionary = new Dictionary<string, string>();
string pluginsDirectory =
    Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\plugins";
string[] pluginFiles = getPluginFilePaths(pluginsDirectory);
LoadPlugins(pluginFiles, pluginDictionary);

public static string[] getPluginFilePaths(string pluginsDirectory)
{
    if (Directory.Exists(pluginsDirectory) == false)
        throw new DirectoryNotFoundException
            ("\"plugins\" foler is not found.\n" + pluginsDirectory);

    return Directory.GetFiles(pluginsDirectory, "*.cs");        
}

public static void LoadPlugins
    (string[] pluginFilePaths, Dictionary<string, string> pluginsDictionary)
{
    foreach(string pluginFilePath in pluginFilePaths)
        pluginsDictionary[Path.GetFileNameWithoutExtension(pluginFilePath)]
            = pluginFilePath;
}

プラグインのコンパイル

プラグインをコンパイルするためにはソースコードを読み込み、ソースコードを解釈し、必要ならパラメータを与える必要があります。 .Netframework にはそれぞれのためのクラスがあらかじめ用意されています。

ソースコード読み込みとコンパイルの機能は、 CodeDomProvider クラスによって提供されています。また、CodeDomProvider によってコンパイルする際に必要なパラメータは、CompilerParameters クラスを利用します。

コンパイルパラメータの設定

まず注目するのは CompilerParameters の設定です。ここではプログラムの実行中にプラグインの機能を利用したいので、プラグインをコンパイルした結果は、メモリ上に展開する必要があります。メモリ上に展開するためには、CompilerParameters のプロパティ GenerateInMemory に true を設定します。

次に、プラグインの中で、System.dll の機能を利用したいので、アセンブリの参照を追加します。例えば、従来は、ソースコード中に、"using System;" と書きますが、これはプロジェクトそのものに System.dll への参照が適用されているからです。したがって、プラグイン中で、"using System;" を利用したいときは、プラグインのコンパイル時に System.dll への参照を追加する必要があります。

public static Assembly CompilePlugin(string pluginFilePath)
{
    CodeDomProvider codeDomProvider = new Microsoft.CSharp.CSharpCodeProvider();
    CompilerParameters compilerParameters
        = new CompilerParameters() { GenerateInMemory = true };
    compilerParameters.ReferencedAssemblies.Add("System.dll");
    CompilerResults compilerResults;

    compilerResults = codeDomProvider.CompileAssemblyFromFile
        (compilerParameters, pluginFilePath);

    if (compilerResults.Errors.Count > 0)
    {
        string compileErrorText =
            "Failed to compile script :\"" + pluginFilePath + "\"\n";

        foreach (CompilerError compilerError in compilerResults.Errors)
            compileErrorText += compilerError + "\n";

        throw new ApplicationException(compileErrorText);
    }

    return compilerResults.CompiledAssembly;
}

コンパイルとその結果

CodeDomProvider を利用してコンパイルを実行する際は、コンパイルするソースコードと、パラメータを指定します。都合が良いことにファイルからソースコードを読み込んでコンパイルするためのメソッド、CompileAssemblyFromFile が用意されています。ここではこのメソッドを利用します。引数に先に設定した CompilerParameters と、ソースコードのファイルパスを指定します。

コンパイルの実行結果は、CompilerResults 型のインスタンスとして与えられます。このインスタンスがプラグインを示すものではない点に注意してください。コンパイルエラーが発生したときは、 Errors プロパティから参照することができます。ここではエラーテキストを返しています。コンパイルに成功するとき、 Errors プロパティの要素数は 0 です。コンパイルした結果はアセンブリとして取得できます。

プラグインの実行

プラグインを実行するアプリケーションとして簡単なものを用意しました。コンソールから End コマンドを入力するまでユーザの入力を受け付けるコンソールアプリケーションです。もしもユーザがコンソールから入力した文字列が、プラグインの名前と一致したら、プラグインを実行します。

ユーザの入力がプラグイン名と一致したら、先に解説した方法でプラグインをコンパイルして Assembly 型のインスタンスを取得します。プラグインとして実装したメソッドを Assembly から実行するために、まずはそのメソッドが実装されたクラスを取得します。ここでは、プラグイン名 = クラス名です。指定したクラスの実装を取得するには、GetType メソッドを利用します。引数はクラス名で、戻り値は Type 型のインスタンスです。

Type 型のインスタンスから任意のメソッドを実行するには、InvokeMember メソッドを利用します。引数は、(実行するメソッド名、BindingFlags.InvokeMethod, null null, 引数となるObject型の配列)です。詳細については msdn を参照してください。

while (true)
{
    Console.Write("Input your command. > ");

    string input = Console.ReadLine();

    if (input == "End")
        break;

    if (pluginDictionary.ContainsKey(input))
    {
        try
        {
            Assembly plugin = CompilePlugin(pluginDictionary[input]);
            plugin.GetType(input).InvokeMember
                ("Action",
                 BindingFlags.InvokeMethod,
                 null,
                 null,
                 new object[] { 1 });
        }
        catch(Exception e)
        {
            //プラグインの実装に誤りがあるなどの理由で実行できないとき
            Console.WriteLine("Plugin \"" + input + "\" has wrong.");
            Console.WriteLine(e);
        }
    }
    else
    {
        Console.WriteLine("Command " + input + " is not implemented.");
    }
}

プログラムの実行中にプラグインをコンパイルしているので、例えば途中で Punch プラグインの Action メソッドの実装を変更して、再度 Punch メソッドを実行すれば、実行結果は変更されます。サンプルには Punch プラグインの他に Kick プラグインと、実行できないプラグイン2つを用意しました。

補足 : 高速な実行のために

先にも補足したように、実行中にソースコードを読み込んでコンパイルする処理は時間がかかります。この時間を短縮する方法はいくつか考えられます。

1つは予めコンパイルしたプラグインを dll などで用意することです。dll から Type 型を取得すれば同じように Invoke することができます。

もう1つは共通の処理をあらかじめ実行しておくことです。例えばプラグインのコンパイルに必要なパラメータが同じであれば、CompilerParameter はコンパイルの実行の度に新しいインスタンスを作る必要がありません。

あるいは、dll にはしないものの、実行中にはプラグインを書き換えない、というのであれば、Dictionary に予めコンパイルを終えた Assembly を保存しておくこともできます。さらに、Assembly から対象のメソッドを取得することも少なくない処理を要するので、メソッドの実装を MethodInfo 型のインスタンスとして Dictionary に保存しておくこともできます。MethodInfo は InvokeMember と同じ働きをする Invoke メソッドを持っています。