こんにちは、キャスレーコンサルティングのSI(システム・インテグレーション)部の中尾です。

今回紹介するスクリプトは、「Roslyn for Scripting」と言って
「C#のプログラム内でC#で書かれたスクリプトを実行できる」物です。

初めに

2015年末頃、C#プログラム内から、C#をスクリプト実行することが可能になりました。
「スクリプト実行」と聞くと、JavaScriptの様にコンパイル不要でブラウザ上で動くプログラムや、
VBScriptの様にテキストファイルでプログラムを書いてファイルをダブルクリックで即実行される物を
思い浮かべるかと思いますが、今回紹介するスクリプト「Roslyn for Scripting」は
結局C#プログラミング上で動くのでコンパイルは必要だったりします。

が、どうかがっかりせず続きを読んでみて下さい。

背景

今まで、C#のコンパイラは「csc.exe」として、.NET Frameworkに付属されていました。
実装されたプログラムのソースコードの構文解析は当然コンパイラが行いますが、
Visual Studioなどの総合開発環境にてインテリセンスを表示する場合などにはコンパイルする前の開発の段階でも
ソースコードを解析する必要があり、この解析はcsc.exeとは「別に」作られた独自機能として実装されていました。

その為、C#に言語仕様の変更が入った場合などはコンパイラは当然メンテナンスを行いますが、
それとは別でコンパイラ以外の機能でC#の構文解析を行っている機能もメンテナンスを行う必要がありました。

その様な同じ解析機能を多重に管理しなければならない現状を解決する為に、
「Roslyn」と言うC#の構文解析を行うパーサ機能をライブラリとして実装する事となりました。
Roslynが作られたことにより、コンパイラもコンパイラ以外でも構文解析は同じライブラリを使い行う事が
出来るようになり、管理の多重化などの問題が解決される事になりました。

このRoslynの機能を使いC#のプログラム上から別途C#のプログラムの構文解析が行えるようになった為、
「Roslyn for Scripting」と言うスクリプト実行機能が提供される事となりました。

「Microsoft.CodeAnalysis.CSharp.Scripting」の参照の追加

C#のプログラムをスクリプトとして動的に実行する方法を紹介したいと思います。

スクリプト実装機能は通常の.Net Frameworkには含まれていない為、
別途NuGetパッケージマネージャーより「Microsoft.CodeAnalysis.CSharp.Scripting」の参照を追加する必要があります。

まず普通にプロジェクトを作りましょう。今回は単純にコンソールアプリケーションを作ります。

画像01

スクリプト実行は.Net Framework 4.6より使用出来る為、プロジェクトのプロパティを開き、
対象のフレームワークを「.Net Framework 4.6」に上げましょう。

画像02

メニューのツール→NuGet パッケージマネージャー→パッケージ マネージャー コンソールを選択し、
パッケージ マネージャー コンソールのウィンドウを開きます。

画像03

パッケージ マネージャー コンソールより、
「Microsoft.CodeAnalysis.CSharp.Scripting」のパッケージをインストールします。

「Install-Package Microsoft.CodeAnalysis.CSharp.Scripting」と打ち込んで下さい。

画像04

「’Microsoft.CodeAnalysis.CSharp.Scripting 1.1.1′ が ConsoleApplication1 に正常にインストールされました」
と表示されれば参照の追加は正常終了です。

画像05

対象のフレームワークを4.6に上げないで行うと、
下記の様にインストール出来ませんでしたというメッセージが出ますので気をつけて下さい。

画像06

ソースコードに「using Microsoft.CodeAnalysis.CSharp.Scripting;」とusingを追加すればライブラリが使用可能です。

ライブラリ説明、簡単なサンプルプログラム

スクリプト機能は基本的に「CSharpScript」クラスを主に使用します。
代表的なメソッドとして用意されている以下の2つを使ってみます。

RunAsync:処理を実行する
EvaluateAsync:処理を実行して戻り値を取得する

using Microsoft.CodeAnalysis.CSharp.Scripting;
using System;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {

            CSharpScript.RunAsync("System.Console.WriteLine(\"Hello Roslyn For Scripting!!\");").Wait();

            Task<double> result = CSharpScript.EvaluateAsync<double>(@"
                var pi = 3.14;
                var r = 5;
                pi * r * r
                ");
            result.Wait();
            Console.WriteLine(result.Result);
        }
    }
}

実行結果
画像07

基本的には、引数に実行したいスクリプトをString型として渡すだけで実行出来ます。

RunAsyncメソッドは引数のスクリプトをそのまま実行します。
「CSharpScript.RunAsync(“System.Console.WriteLine(\”Hello Roslyn For Scripting!!\”)”).Wait();」にて、
WriteLineメソッドに渡した文字列がコンソールに出力されています。

EvaluateAsyncメソッドは引数のスクリプトをそのまま実行し、
実行した結果をResultプロパティに格納したTaskオブジェクトを戻り値として返します。
Tは戻り値の型となります。
「Task result = CSharpScript.EvaluateAsync(中略)」にて、
スクリプトを実行して最終的に「pi * r * r」の値が返されます。
返す値は頭にreturnをつけたり末尾に;をつけたりするとエラーとなるので気をつけて下さい。

Task result = CSharpScript.EvaluateAsync(@"
      var pi = 3.14;
      var r = 5;
      return pi * r * r
    "); // ↑失敗します

 

Task result = CSharpScript.EvaluateAsync(@"
      var pi = 3.14;
      var r = 5;
      pi * r * r;
    "); // ↑失敗します

globalsというパラメータにスクリプトを実行対象とするオブジェクトを渡すことで、そのオブジェクトに対してスクリプトを実行する事ができます。

using Microsoft.CodeAnalysis.CSharp.Scripting;
using System;
using System.Collections.Generic;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            List<String> strList = new List<String>();

            // 通常のインスタンスメソッド呼び出し
            strList.Add("プログラムから文字列追加");

            // オブジェクトに対してスクリプトを利用したインスタンスメソッド呼び出し
            CSharpScript.RunAsync("Add(\"スクリプトから文字列追加\");", globals:strList).Wait();

            // List<String>に格納されている文字列を出力
            foreach (String str in strList)
            {
                Console.WriteLine(str);
            }
        }
    }
}

実行結果

画像08

「CSharpScript.RunAsync(“Add(\”スクリプトから文字列追加\”);”, globals:strList).Wait();」にて、
globalsで渡したstrListに対してスクリプトを実行した形になっています。
実質、リフレクションでメソッド実行しているのと同じ様な動きをすることになります。

注意点としては以下の物があります。
実行するスクリプトは「System」などの名前空間はusingされていない状態で実行される為、
使用するクラスはフルパスで記述しなければなりません。
×:var result = await CSharpScript.RunAsync(“Console.WriteLine(\”Hello Roslyn For Scripting!!\”)”);
○:var result = await CSharpScript.RunAsync(“System.Console.WriteLine(\”Hello Roslyn For Scripting!!\”)”);

メソッドは非同期処理として実装されている為、戻り値の型はTaskになります。
適時Wait();なりを呼んで非同期処理としての対応を行って下さい。

電卓を作ってみよう

一般的な電卓と言えば、テンキーと+や-などの演算機能のボタンが配置されており、
ボタンをぽちぽち押して操作しますが、押し間違いをしてしまったり、
押し間違えた事に気づかず計算を続けてどこで誤りが発生したかよく分からなくなる事がありますよね?

スクリプト機能のサンプルとして、プログラム上で計算結果を変数へ代入する処理を
スクリプト上で行うイメージで動作する電卓を作ってみます。

画面イメージは以下の様になります。

画像09

変数とその変数に代入する計算式を入力するのを繰り返し、
一度変数に入力した値は後続の入力にて式の中で使用する事が出来る動きにしました。

ソースコードは以下の通りになります。ついでに「clear():変数一覧のクリア」メソッドと
「delete(valueName):指定した変数の削除」メソッドも実装してみました。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace Calculator
{
    public partial class CasleyCalculator : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        /// <summary>
        /// 計算式テキストボックスにてEnterが押下された時に、変数と計算式を元に各処理を行います
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void textBoxCalculate_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
        {
            if (e.KeyCode != Keys.Return)
            {
                return;
            }

            if (this.textBoxCalculate.Text.StartsWith("clear(") || this.textBoxCalculate.Text.StartsWith("delete("))
            {
                // this に対するclaer()およびdelete()メソッドのスクリプト実行
                CSharpScript.RunAsync(this.textBoxCalculate.Text, globals: this).Wait();
                this.labelMessage.Text = String.Empty;
                this.textBoxValue.Text = String.Empty;
            }
            else
            {
                // 単純な数値計算のスクリプト実行
                MainAsync(this.textBoxValue.Text, this.textBoxCalculate.Text).Wait();
                this.labelMessage.Text = String.Empty;
                this.textBoxValue.Text = String.Empty;
            }
        }

        /// <summary>
        /// 単純な数値計算のスクリプト実行
        /// </summary>
        /// <param name="valueName">変数名</param>
        /// <param name="method">計算式</param>
        /// <returns>非同期操作オブジェクト</returns>
        private async Task MainAsync(String valueName, String method)
        {
            String temp = String.Empty;

            // 今まで計算した計算式をリスト化する
            foreach (ListViewItem item in this.listView.Items)
            {
                temp += $"var {item.Text} = {item.SubItems[1].Text};";
            }

            // 今回の計算式を結合
            temp += $" {method}";

            var result = await CSharpScript.EvaluateAsync(temp);

            this.listView.Items.Add(new ListViewItem(new String[] { valueName, result.ToString(), result.GetType().Name, method }));
        }

        /// <summary>
        /// リストのクリア
        /// </summary>
        public void clear()
        {
            this.listView.Items.Clear();
        }

        /// <summary>
        /// 指定した変数の削除
        /// </summary>
        /// <param name="valueName">削除する変数名</param>
        public void delete(String valueName)
        {
            foreach (ListViewItem item in this.listView.Items)
            {
                if (item.Text == valueName)
                {
                    this.listView.Items.Remove(item);
                    break;
                }
            }
        }
    }
}

実行イメージは以下の通り。

画像10

円の面積を求めてみましょう。

円周率をpiに設定します。

画像11
画像12

半径をrを5に設定します。

画像13
画像14

おっと間違えました。変数rを削除します。

画像15
画像16

改めて半径をrを5に設定します。

画像17
画像18

面積を求めましょう。

画像19
画像20
半径が1~5の5個の円の面積の合計を求めましょう。
画像21
画像22

この様にC#の構文を利用した複雑な計算が出来る点がこの電卓の特徴になります。
一覧をクリアしましょう。

画像23
画像24

終わりに

ざっくり簡単にスクリプトの機能を紹介させてもらいました。
サンプルではCSharpScript.RunAsyncとCSharpScript.EvaluateAsyncの二つのメソッドだけで
ごく簡単な入門的な使い方しか紹介しませんでしたが、以下の様な機能も用意されており、
それらを駆使するともっとスクリプト実行の幅が広がると思うので、興味を持った方は調べてみて下さい。

・名前空間の追加、参照アセンブリの追加(AddReferences)
・複数のスクリプトを実行結果を引き継ぎながら実行(ContinueWithAsync/ContinueWith)

冒頭で書いたとおり「スクリプトといっても結局はコンパイルは必要」なのですが、
いずれはVBScriptの様にダブルクリックで単体実行が出来るようになり、
もっと手軽にC#を使える環境が出来れば良いなと思っています。