Roslyn for Scripting を使うと、自作プログラムからC#で書いたプログラム(スクリプト)をビルドせず即実行できるようになります。
今回は、その基本的な使い方について要点を解説したいと思います。
また、Roslyn for Scriptiong と相性の良いテキストエディタについて「【WPF】C#スクリプト・エディター「RoslynPad」を組み込む」で詳しく紹介しています。ソースコードの補完機能(インテリセンス)が使えますので併せて活用下さい。
Roslyn for Scripting とは
Roslyn(ロズリン)とは、Visual Studio 2015で採用されたコンパイラープラットフォームのコードネームです。
コンパイラーに必要な機能(言語解析、シンタックハイライト、IntelliSense(プログラム補完機能)、リアルタイムエラー解析、リファクタリングなど)を網羅し、APIとして外部から呼び出されることを前提に開発されました。
Roslyn for Scriptin はこの中の1つの機能であり、C#言語の構文解析を行いながらリアルタイムで実行できる「スクリプト実行環境」を提供してくれます。
Roslyn for Scriptin を自作プログラムに組み込むことで、例えば「C#のプログラムをテキストボックスに貼り付け、ボタンを押すことで実行結果を画面に表示する」といったことが簡単に実現できます。
つまり、後から修正が発生するような処理をC#スクリプトとして切り離しておくことで、プログラムのビルドを必要とせず、いつでも処理の追加/変更が可能になります。
たとえVisual Studioがインストールされていない環境であっても、すぐさま処理を変更することが出来るのです。
インストール方法
NuGetでプロジェクトにインストールします。
VisualStudioのメニューから「ツール」→「NuGetパッケージマネージャ」→「ソリューションのNuGetパッケージの管理」でNuGetのタブが表示されますので、下記のキーワードで検索してください。
Microsoft.CodeAnalysis.CSharp.Scripting
似たようなものがいくつも表示されますが、一番上に「Microsoft.CodeAnalysis.CSharp.Scripting」が表示されているので、これを選択します。
NuGetの使い方がわからない方は、「Visual Studio のパッケージ管理機能 NuGetとは? | 初心者DIYプログラミング入門」の記事をご覧ください。
Roslyn for Scripting の使い方
C#スクリプトを実行するには、 CSharpScript クラス(staticクラス)のメソッドを使います。このメソッドは3種類が用意されていて、それぞれ次の特徴と役割を持っています。
C#スクリプトの実行方法 | 解説 |
---|---|
CSharpScript.Create()でインスタンスを生成 そのインスタンスのRunSync()を実行 | 渡されたC#スクリプトをCreate()で事前にビルドし、RynSync()で実行します。 同じC#スクリプトを何度も実行する場合、ビルドに掛かる時間が1回で済みます。 |
CSharpScript.RunSync()を実行 | 渡されたC#スクリプト全体をビルド後に即実行します。 また、実行が終わるまで処理はブロックされ、C#スクリプト内で実行した変数などは終了後に消滅します。 |
CSharpScript.EvaluateAsync()を実行 | 渡されたC#スクリプト全体をビルド後に即実行しますが、処理はブロックされず、変数の値は保持されます。 変数や式の値を参照したり、書き換えることが可能なので、対話型の実行環境として利用できます。 |
本章では、CSharpScript.Create()とRunSync() を使った実行方法について解説します。
Roslyn for Scripting の主なメソッド
C#スクリプトを実行するには、次の2つを使用します。
引数の使い方については後ほど詳しく説明しますので、ここでは概要だけ理解しておいて下さい。
メソッド名 | 戻り値 | 説明 |
---|---|---|
Create( string code, ScriptOptions options, Type globalsType, InteractiveAssemblyLoader assemblyLoader ) | script<object> | Roslyn for Scriptingで新しいインスタンスを作成しますが、RunAsyncを呼ぶまでは実行されません。 code:実行したいスクリプト ScriptOptions:実行時のパラメータ等 globalType:外部参照するクラスの型 assemblyLoader:参照したいDLLなど |
RunAsync( object globals, CancellationToken cancellationToken) | Task<T> | Createで指定されたスクリプトを実行し、結果を返します。 globals:参照するクラスインスタンス cancellationToken:強制終了のトリガ |
C#スクリプトの実行
あらかじめ、次の参照設定を冒頭に記述しておきます。
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
次に、CSharpScriptのCreateメソッドに実行したいC#スクリプトを渡してインスタンスを生成します。
そして、そのインスタンスの RunASyncメソッドを呼び出すことで、C#スクリプトが実行されます。
// 実行したいC#スクリプトを変数に格納
var program = "var i = 100; return i * 5;";
//実行したいスクリプトを渡してCSharpScriptのインスタンスを生成
var script = CSharpScript.Create(program);
//C#スクリプトを実行し、結果を受け取る
var result = script.RunAsync().Result;
C#スクリプトからの戻り値を受け取る
C#スクリプト側から呼び出し側に戻り値を返す場合、return を使います。
例えば、次のようなスクリプトを実行すると、戻り値として 300 が返されます。
int a = 100;
int b = 200;
return a + b;
戻り値は RunAsync メソッドから返される ScriptState オブジェクトのResultValueプロパティで取得できます。
この時、受取側ではC#スクリプトが返す値の型に応じて適切にキャストする必要があります。
今回の例では a , b ともに int 型なので、次のようになります。
int val = (int)result.ReturnValue;
もし型を間違えるとエラーになりますし、C#スクリプト側で return を記述し忘れた場合、ResultValue には null が返されるので注意が必要です。
受取時に型を気にしなくて済むようにするには、次のように文字列にキャストて受取り、必要に応じてTryPhaseで変換できるか試してみるというのが一番楽かと思います。
var script = CSharpScript.Create(script);
var result = script.RunAsync().Result;
string res = (result.ReturnValue == null) ? "" : result.ReturnValue.ToString();
スクリプト側で任意のクラスを受け取って使用する
C#スクリプトの実行時、任意のクラスを1つだけ引数として渡すことが可能です。渡されたクラスのプロパティやメソッドは、C#スクリプトの中から自由にアクセスできます。
このクラスを通じて、C#スクリプトの呼び出し元とC#スクリプト内で様々なやり取りが可能です。
具体的には、CSharpScript.Create メソッドの引数にクラスの型を指定し、RunAsync メソッドの引数にクラスの実態(インスタンス)を指定します。
例えば、次のようなMyArgsクラスがあったとして、これをC#スクリプトに渡すことを考えてみます。
//C#スクリプトの引数として渡すクラス
public class MyArgs
{
public int MyValue { get; set; }
public int MyMethod(int value)
{
return value * value * value;
}
}
まず最初に new MyArgs() でインスタンスを作成します。
次に、CSharpScript.Createメソッドの globalsTypeという引数名にtype(MyArgs)で型を指定し、RunAsyncメソッドの第一引数にインスタンスを指定すればOKです。
var args = new MyArgs();
args.MyValue = 100;
var script = CSharpScript.Create(uxScript.Text, globalsType: typeof(MyArgs));
var result = script.RunAsync(args).Result;
uxResult.Text = (result.ReturnValue == null) ? "" : result.ReturnValue.ToString();
C#スクリプト側で、次のようなスクリプトを実行すると、スクリプト終了後、args.MyValue の値が1000に書き換わっています。
MyValue = MyMethod(10);
この様に、C#スクリプト側では、あたかも最初からMyValueやMyMethod が備わっていたかのように利用することが出来ます。
スクリプトの中で .NET標準のDLL(アセンブリ)を利用する
初期設定の状態では、基本的なクラス(Systemなど)しかスクリプト上で利用できません。例えば、DataTableや File、PathなどのIO関係のクラス、Dynamic型やLinq などは呼び出せません。
これらを使いたい場合は、使いたいDLLのアセンブリ参照を明示的に記述する必要があります。
まず最初に次の参照設定を冒頭に記述しておきます。
using System.Reflection;
手順は次の様になります。
- 読み込みたいアセンブリのリストを作成(List<Assembly> assembly)
- C#スクリプトで使いたいアセンブリのリストを作成(List<string> import)
- 上記ScriptOptionsクラスに上記2つを登録
- CSharpScript.Create メソッドの引数にScriptOptionsのインスタンスを指定
以下が実際のソースコードです。
このままコピペしていただければ、.NETで用意されている主なクラスは使えるようになります。
private void uxExec_Click(object sender, RoutedEventArgs e)
{
//アセンブリの読み込み
List<Assembly> assembly = new List<Assembly>()
{
Assembly.GetAssembly(typeof(System.Dynamic.DynamicObject)), // System.Code
Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)), // Microsoft.CSharp
Assembly.GetAssembly(typeof(System.Dynamic.ExpandoObject)),
Assembly.GetAssembly(typeof(System.Data.DataTable))
};
//C#スクリプトのインタプリタにインポートするアセンブリの指定
List<string> import = new List<string>()
{
"System","System.Dynamic", "System.Linq", "System.Text","System.IO", "System.Collections.Generic", "System.Data"
};
//C#スクリプトのオプションパラメータにアセンブリとインポートのリストを登録
var option = ScriptOptions.Default.AddReferences(assembly).AddImports(import);
var script = CSharpScript.Create(uxScript.Text,options: option);
var result = script.RunAsync().Result;
uxResult.Text = (result.ReturnValue == null) ? "" : result.ReturnValue.ToString();
}
スクリプトの中で自作DLL(自作クラスなどのアセンブリ)を利用する
自作の共通クラスをC#スクリプトから利用したい場合、Assembly.LoadFromを使ってDLLファイルのパスを指定します。
さらに、List<string>でもアセンブリ名を指定します。
List<Assembly> assembly = new List<Assembly>()
{
Assembly.LoadFrom("d:\MyCommon.dll")),
Assembly.LoadFrom("d:\MyFileIO.dll"))
};
List<string> import = new List<string>()
{
"MyCommon","MyFileIO"
};
C#スクリプトの実行を強制的に停止させる
C#スクリプトを強制停止させるには、 CancellationTokenSourceクラスのインスタンスを生成し、Tokenプロパティの値をRunAsyncのcancellationToken引数に渡します。
// CancellationTokenSourceノインスタンスを生成し、Tokenプロパティを取得
var cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
//Tokenプロパティの値をRunAsyncに渡す
var scriptState = await CSharpScript.RunAsync(script, options: scriptOptions, cancellationToken: cancellationToken);
そして、停止させたいタイミングで、CancellationTokenSourceインスタンスの Cancelメソッドを呼び出します。
// スクリプトの実行をキャンセルする
cancellationTokenSource.Cancel();
通常が画面からキャンセルボタンを押してスクリプトの実行を停止するケースが多いと思います。この場合は、RunAsyncを非同期で実行し、キャンセルボタンの処理で cancellationTokenSource.Cancel() を実行すると良いでしょう。
詳細はこちらの公式ページに記載されています。
C#スクリプトの実行をメソッド化したサンプル
今までの解説をすべて盛り込んだ形で、C#スクリプトを簡単に実行できるメソッドをサンプルとして掲載しておきます。
C#スクリプト側でエラー(例外)が発生するとプログラムが落ちてしまうので、Try~Catch で例外を握りつぶしています。
public string ExecScript(string source, Type argstype, object args)
{
try
{
List<Assembly> assembly = new List<Assembly>()
{
Assembly.GetAssembly(typeof(System.Dynamic.DynamicObject)), // System.Code
Assembly.GetAssembly(typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo)), // Microsoft.CSharp
Assembly.GetAssembly(typeof(System.Dynamic.ExpandoObject)),
Assembly.GetAssembly(typeof(System.Data.DataTable)),
};
List<string> import = new List<string>()
{
"System","System.Dynamic", "System.Linq", "System.Text","System.IO",
"System.Collections.Generic", "System.Data"
};
var opt = ScriptOptions.Default.AddReferences(assembly).AddImports(import);
var script = CSharpScript.Create(source, globalsType: argstype, options: opt);
var result = script.RunAsync(globals: args).Result;
var value = result.ReturnValue;
return (value == null) ? "" : value.ToString();
}
catch
{
}
return "";
}
使い方は次の様になります。
var args = new MyArgs();
args.MyValue = 5;
ExecScript(uxScript.Text, typeof(MyArgs), args);
val res = args.MyValue;
まとめ
Roslyn for Scripting を使うことで、自作プログラムの中からC#スクリプトを実行し、結果を得ることが出来るようになります。
C#スクリプトの呼び出し元とスクリプト内のデータの受け渡しは、任意のクラスを介して行うことが可能で、.NETで用意されている様々なクラスや、自作クラスでさえC#スクリプトから利用することが可能です。
非常に強力な機能ですので、是非ご活用ください。