真・Roslyn for Scripting! あなたのアプリケーションにもC#スクリプトを!!

roslynこの記事は「C# Advent Calendar 2015」の18日目の記事です(遅刻しました (;´Д`))。
そして、この記事は以前に書いた「Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!!」の改訂版なので、先にこちらを眺めてもらえるとスムーズに理解できると思います。


Roslyn for Scriptingの正式版が公開されました!

前記事ではまだ正式版ではありませんでしたが、とうとう正式版が公開されました。この記事では、以前のバージョンと正式版との違いについて焦点を当てたいと思います。

背景

前回の記事で紹介したNuGetのパッケージは、Visual Studio 2015リリースに向けての追い込みの時期に公開されたものです。バージョンは1.0.0-rc2なので、Visual Studio 2015に合わせて正式リリースするつもりであるように見えました。ただ、バックログを見ても色々課題が残っており、本当にあと少しで間に合うのかという不安もあったのです。

すると、何とRoslynのScripting部分はキャンセルされてしまいました!! rc3でそれらのコードはごっそり削除、NuGetの更新も途絶えてしまったのです。

私が知っている(issueにも挙げた)課題としては、以下のようなものがありました。

  • 外部から与えるグローバルインスタンスがDynamicObjectの場合でも、スクリプト環境はそれを認識できない。
  • メソッドの即時実行を行った場合に、非同期メソッドであった場合は、グローバルなクロージャが非同期コンテキストを認識できない。
  • スクリプティング環境では、Roslyn自体は内包されていてアクセス出来ない。IntelliSenseみたいな事をしたくても出来ない

# グロサミで私の写真がちらっと出た時はびっくりしました。ちょっと嬉しかった

これらがどのように解決されたのかも検証したいと思います。


インターフェイスの変更

正式版のバージョンは1.1.1です。これは、Visual Studio 2015 Update1とともにリリースされました。ただし、RoslynやScriptingのNuGetパッケージは独立しているので、VSのバージョンと関係なく個別に使用することができます。出来るのですが… 実際には.NET Framework 4.6以上が必要です。NuGetのパッケージのターゲットフレームワークが「dotnet」となっており、いわゆる「netcore5」に対応するのですが、従来の環境では4.6以上が必要になります。

実現内容を考えると、もっと古いターゲットでも動作させられる可能性はあると思うのですが、netcore5から導入されたライブラリのNuGet細分化を前提としているため、移植は別の意味で面倒かもしれません。

パッケージ名は「Microsoft.CodeAnalysis.CSharp.Scripting」です。NuGetでサクッとインストールしましょう。

PM> Install-Package Microsoft.CodeAnalysis.CSharp.Scripting

さて、正式版で変更を受けたポイントは2つあります。

  • 時間のかかる処理は、すべて非同期(Task)ベースとなりました。
  • デフォルトオプションは完全に「空」になりました。

時間のかかる処理とは、Scriptingで言うならRunメソッドです。rc2ではRunメソッドを呼び出すと、スクリプトが実行されて結果が返却されました。この部分がTaskベースの非同期処理となり、名前も「RunAsync」メソッドとなりました。後方互換性は維持されていないため、正式版を使用する場合は修正が必要です。とは言っても、難しいことは無いでしょう。

// 指定されたスクリプトを非同期で実行する
public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");
	
	var script = CSharpScript.Create("Console.WriteLine(\"Hello C# scripting!\");");
	await script.RunAsync();

	Trace.Write("Finished script");
}

上の例のように、RunAsyncメソッドをawaitで待機します。あるいは、戻り値のTaskを使ってゴニョゴニョすればOKです。合理的な変更だと思います。また、Createメソッドはジェネリックバージョンもあり、RunAsyncの戻り値を型指定で伝搬させ、スクリプトの式評価の結果をタイプセーフに得る事もできます。この辺りも「こうなればいいのに」と思っていたことが実現されていて好感が持てます。

consolenotfoundさて、実は上のコードは動作しません。RunAsync呼び出し時に例外が発生します。

例外の内容を見ると、「Console」が見つからないと出力されています。rc2では、デフォルトのインポート節(C#で言うところのusing句)として「System」が含まれていたのですが、正式版ではこれが削除されて、何もインポートされていない状態となっています。そのため、自分でインポートを追加する必要があります(または、スクリプトコードにusing句を追加するか、完全限定名でクラスを指定します)。

修正したコードは以下の通りです。

// 指定されたスクリプトを非同期で実行する
public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");
	
	var script = CSharpScript.Create("Console.WriteLine(\"Hello C# scripting!\");").
		WithOptions(ScriptOptions.Default.
			WithImports("System").
			WithReferences(typeof(object).Assembly));
	await script.RunAsync();

	Trace.Write("Finished script");
}

WithOptionsメソッドで、スクリプトにオプションを指定できます。前回の記事ではインポート(名前空間)と参照を加えていましたが、ここの扱いは今回も変わっていません。明示的に”System”を追加してあります。参照も追加してありますが、mscorlibの範疇の型は追加しなくても認識出来るようです。


グローバルコードの扱い

グローバルコードの解釈が練られ直されたようです。

前回紹介したように、スクリプティング環境では、単なる

// 以下はスクリプト:
// いきなりvar
var hoge = 123;

// いきなりメソッド呼び出し
Console.WriteLine("ABC");

のように、名前空間の指定もクラスや構造体もメソッドの定義もない所からの実行可能な式や文を許容します(許容する仕様です)。これが満たされないと、スクリプティングの逐次実行(要するにインタプリタのように使う)が出来ないので、魅力半減です。

このような文は、内部でコード生成された時点で、不透明な外郭クロージャクラスとクロージャメソッドが作られ、その中で文を実行する事になります。

所で、次のようなスクリプトを考えてみます。

// 以下はスクリプト:
using (var httpClient = new HttpClient())
{
	return await httpClient.GetStringAsync("http://www.bing.com/");
}

このコードの戻り値はTask<string>となります。コード中でawaitを使っているからなのですが、これも通常のコードとしてコンパイルする場合は、メソッドシグネチャに「async」と書かないとコンパイルエラーになります。同じRoslynを使ってパースしているのですが、グローバルコードではasyncを書かなくても(書けないが)、awaitを認識して、Taskクラスのインスタンスとして返却するのです。

この仕様はかなり奇妙ですが、コードを書かれるまでは、それが同期的なのか非同期的なのかが分からないので、仕方ないと割り切った感があります(これを許すなら、互換性のためのasync句とは何だったのかという気もしなくもない)。

クロージャーとなる実装がどのようなものかを、簡単に眺めてみます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	var script = CSharpScript.Create(
@"var sf = new StackFrame();
var method = sf.GetMethod();
var type = method.DeclaringType;
Console.WriteLine(""{0} - {1}"", type.FullName, method.Name);
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Diagnostics",
				"System.Reflection").
			WithReferences(typeof(object).Assembly));

	await script.RunAsync();
	Trace.Write("Finished script");
}

この結果、コンソールにはこのように表示されました:

Submission#0+<<Initialize>>d__0 - MoveNext

興味深い事です。クロージャーの実装は、”Submission#0″クラスのインナークラス”<>d__0″に作られた”MoveNext”メソッドのようです。これは、C#コンパイラがAwaitableなステートマシンを作るときの形に似ているので、おそらくそのまま再現されているのでしょう。そして、仮にスクリプトブロック内にawaitを使わなかったとしても、初めからasync-awaitが有効なコードブロックとして評価している事が分かります。

しかし、問題はそれだけではありません。「遅延実行」が絡む場合は、いつ実際に評価を行うのかが難しいのです。前回の説明で逐次処理を行う方法を説明しましたが、2つのawaitが分割されて処理されたらどうなるのでしょうか?

// 以下はスクリプト:
// 最初の処理単位(わざとusingを使わない)
var httpClient = new HttpClient();
await httpClient.GetStringAsync("http://www.bing.com/");

// (逐次処理評価で分断)

// 2回目の継続処理単位
await httpClient.GetStringAsync("http://www.google.com/");

なまじ、async-awaitがクロージャーに内包されて実行されている事を知っていると、このコード片がどのように実行されるのかに迷いが生じます。つまり、最初と2回目の継続処理は、単一のクロージャーにまとめられる必要があるのではないかと。しかし、「スクリプトである」と考えれば明確で、逐次処理単位毎に実行されます(つまり書いた通りの感じで逐次実行される)。

逐次処理の検証をしてみます。ScriptStateのAPIも整理されました。RunAsyncの結果がScriptStateなので、このクラスのContinueWithAsyncメソッドを使うと簡単に書けます:

public static async Task ExecuteScriptAsync3()
{
	Trace.WriteLine("Start script...");

	var script1 = CSharpScript.Create(
@"var httpClient = new HttpClient();
await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Net.Http").
			WithReferences(
				typeof(object).Assembly,
				typeof(HttpClient).Assembly));

	var scriptState1 = await script1.RunAsync();

	var scriptState2 = await scriptState1.ContinueWithAsync(
@"await httpClient.GetStringAsync(""http://www.google.com/"");
");

	Trace.Write("Finished script");
}

これで、最初のRunAsyncでBingが、次のContinueWithAsyncでGoogleが参照されます。しかし、結果は返されません。「return」句を入れると、スクリプトとしてのコードの性質がはっきりします。

public static async Task ExecuteScriptAsync3()
{
	Trace.WriteLine("Start script...");

	// GetStringAsyncをawait後にreturnする
	var script1 = CSharpScript.Create(
@"var httpClient = new HttpClient();
return await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(ScriptOptions.Default.
			WithImports(
				"System",
				"System.Net.Http").
			WithReferences(
				typeof(object).Assembly,
				typeof(HttpClient).Assembly));

	// 戻り値を得る
	var scriptState1 = await script1.RunAsync();
	var result1 = scriptState1.ReturnValue;

	// GetStringAsyncをawait後にreturnする
	var scriptState2 = await scriptState1.ContinueWithAsync(
@"return await httpClient.GetStringAsync(""http://www.google.com/"");
");
	var result2 = scriptState2.ReturnValue;

	Trace.Write("Finished script");
}

結局、逐次処理の単位毎に、式の結果として評価された値が返される事が分かります。同時に、逐次処理させても、内部のクロージャーは包含された大きなメソッドを作るわけではなく分割され、しかし定義された変数は引き続き使えるようにしているのです。

このような挙動を見ていると、同じC#でRoslynでありながら、直観的なコードはかなり感じが違うなと思いました。勿論、メソッドやクラスを定義する事は可能なので、そこまでやればコンパイル前提のC#のコードと変わりませんが…


DynamicObjectの扱い

ホスト環境との通信として、グローバルにアクセス可能なインスタンスを用意して提供すると、スクリプトからアクセス出来る、という機能がありました。新しいバージョンでも引き続きサポートされていますが、その時こんなことを考えました:

// ホストオブジェクトクラス
public sealed class HostObject
{
	public HostObject()
	{
		this.Target = new ExpandoObject();
		this.Target.Id = 123;
		this.Target.Name = "ABC";
	}

	// ExpandoObjectを使ってダイナミックアクセスを指定するホスト環境のプロパティ
	public dynamic Target
	{
		get;
		private set;
	}
}

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	// HostObject.Targetにアクセスする
	var script1 = CSharpScript.Create(
@"Console.WriteLine(""Id={0}, Name={1}"", Target.Id, Target.Name);
",
		ScriptOptions.Default.
		WithImports(
			"System",
			"System.Diagnostics").
		WithReferences(
			typeof(object).Assembly,
			typeof(Microsoft.CSharp.RuntimeBinder.Binder).Assembly),
		typeof(HostObject));	// ホストオブジェクトの型を指定

	// ホストオブジェクト
	var hostObject = new HostObject();

	await script1.RunAsync(hostObject);

	Trace.Write("Finished script");
}

dynamicidnotfoundRunメソッドからRunAsyncメソッドに変わりましたが、引数にホストオブジェクトを渡すことが可能な点は変わっていません。但し、ホストオブジェクトの型を明示的に指定する必要があります。おそらく、C#スクリプトから見て、「静的」には何の型に見えるのかを明示する必要があるのでしょう。CSharpScript.Createに引数で指定します(例ではオプションの指定も引数に寄せてあります)。

また、Microsoft.CSharp.RuntimeBinder.Binderクラスのアセンブリを参照していますが、これは”Microsoft.CSharp”アセンブリを使うためで、要するにDLR対応です(dynamicキーワードを使う場合に必要)。

rc2では上記のコードを書いた場合、IdとNameプロパティには「アクセス出来ません」でした。issueに上げたのですが、どうやら今回も対応しなかったようです。これが出来ると、ホスト側のアクセス可能な要素を動的に定義できると思ったんですけどね。結構根が深そうです。


AST(抽象構文木)の参照

Roslyn for Scriptingは、当然内部でRoslynを使用しているので、スクリプトのパース結果はRoslynの抽象構文木で表現されているはずです。rc2では一体それがどこからアクセス可能なのか良くわかりませんでしたが、1.1.1ではScriptクラスのGetCompilationメソッドからアクセス出来ます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	var script1 = CSharpScript.Create(
@"var target = new
{
	Id = 123,
	Name = ""ABC""
};
Console.WriteLine(""Id={0}, Name={1}"", target.Id, target.Name);
").
		WithOptions(
			ScriptOptions.Default.
			WithImports(
				"System",
				"System.Diagnostics").
			WithReferences(
				typeof(object).Assembly);

	var compilation = script1.GetCompilation();
	var tree = compilation.SyntaxTrees.ElementAt(0);
	var compilationUnitSyntax = (CompilationUnitSyntax)tree.GetRoot();
	var members = compilationUnitSyntax.Members.Select(member => member.GetText().ToString());
	
	Console.WriteLine(string.Join(Environment.NewLine, members));

	Trace.Write("Finished script");
}

ToStringして表示しているだけなので何の捻りもありませんが、パース結果をロジック的に探索出来ます。これを使ってIntelliSenseのような表示の仕掛けも可能でしょう。


各種リゾルバー

スクリプトのコンソールインターフェイスが存在する場合は、ソースコードの参照先パスや、アセンブリの参照先パスを指定して、動的に読み込みをサポートしたくなるかもしれません。これらは通常はScriptOptions.WithFilePathやWithReferencesを使用して指定しますが、スクリプト環境内からの参照解決要求に応答するように実装する事も出来ます。

ScriptOptions.SourceResolverやMetadataResolverがそれに該当し、リゾルバー基底クラスを継承して、独自の参照解決を実装して指定する事が出来ます。

リゾルバーがいつ呼び出されるのか、ですが、Roslyn for Scriptingでは、「REPL」と呼ばれる帯域外コマンドが正式に決定されました。例えば、以下のように「#r」コマンドを使うと、スクリプト内で環境にアセンブリをロードすることが出来ます。

public static async Task ExecuteScriptAsync()
{
	Trace.WriteLine("Start script...");

	// スクリプト内で System.Net.Http アセンブリをロードして使う
	var script1 = CSharpScript.Create(
@"#r ""System.Net.Http""
var httpClient = new System.Net.Http.HttpClient();
return await httpClient.GetStringAsync(""http://www.bing.com/"");
").
		WithOptions(
			ScriptOptions.Default.
			WithImports(
				"System").
			WithReferences(
				typeof(object).Assembly));

	var scriptState = await script1.RunAsync();

	Trace.Write("Finished script");
}

この時、ScriptOptions.MetadataResolverには、RuntimeMetadataReferenceResolverがデフォルトで設定されており、このリゾルバーが標準的な.NETのアセンブリロード処理を行う(この場合は、System.Net.Http.dllをGACから読み取る)ので、#rコマンドが成立するのです。


まとめ

前回と今回の記事で、Roslyn for Scriptingの足掛かりは出来たかなと思います。Roslyn、特にASTの辺りはスクリプティングとは関係なく、これまた膨大なトピックが詰まっていると思います。Roslynで言うならば、上はこのスクリプティングやコンパイラインターフェイス、下はVisual Studio 2015の「アナライザー」が、Roslynへの入り口となると思います。

OSS化され、引き続き改良が進むと思われるため、当分の間はこのインフラが使用される事でしょう。参考になれば幸いです。

それでは。

Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!!

dotnet_logoいよいよ、Visual Studio 2015リリースが近づいてきました。今回はC#的にはあまり大がかりな拡張がありませんが、内情としてはC#コンパイラのインフラが「Roslyn」に正式対応するという事で、地味に大きな変更となっています。

Roslynは、MSのオープンソース戦略としては早い段階で公開され、それ以来、パブリックな場で将来のC#コンパイラの仕様検討などが行われています。勿論、ソースコードも「オープンソース」として公開されており、自分でいじりたければフォークも可能です。そろそろ概要を掴んでおこうと考えている方向けに、いくつかリンクを張っておきます。

なお、この記事には続編があります:「真・Roslyn for Scripting! あなたのアプリケーションにもC#スクリプトを!!」


ちょっとだけ背景

csc今まで、C#のコンパイラは「csc.exe」で、.NET Frameworkに付属していました。このプログラムはアンマネージコード(多分、C++)で書かれており、メンテナンスが大変そうだなと思います。また、Visual Studioが、ソースコードエディタ内でインテリセンスを表示させるために、csc.exeとは「別に」、独自にソースコードのパースを行っており、言語仕様の変化に合わせて二重にメンテナンスしなければならないという問題もありました。更に、Resharperなどのサードパーティプラグインも、この内部実装にはアクセスできないため、またもや同じようなパーサーを独自に書かなければならず、どう考えても不健全です。

そこで、C#のソースコードを解析し、いわゆる「構文木」をオブジェクト表現可能な形でパースする、ニュートラルな独立したライブラリを実装する事にした… というのがRoslynです。当初は「Compiler as a service」 (InfoQ)なワードだけが取りざたされて、何か大きなサービス的なもののように誤解されたこともありましたが、要するにコンパイラのための(低レベルな)インフラをライブラリ化して、様々な環境から共通利用出来るようにしたものです。

# なお、C#, C#と言ってますが、VB.netもRoslyn化されています。

さて、Roslynのパーサーの説明をしようとすると大変そうなので (;´Д`) この記事では「スクリプティング」に焦点を置きたいと思います。

所で、実はこの分野は、.NETのマルチプラットフォーム展開を行う「Mono」の方が先行していて、MonoのC#コンパイラは C#で書かれているので、既に同じようなインフラが存在する事になります。Monoのインフラを使ったスクリプティングインフラとして、「scriptcs」が存在します。このプロジェクトは、スクリプティングのモジュール化に必要な「スクリプトライブラリ」を作る事が出来たり、スクリプトから参照するライブラリをNuGetから取得(実行時に配置、即使用可能)出来たりと、スクリプト環境としての熟成を図っているようです。また、Roslynがリリースされたことで、scriptcsのインフラを、MonoかRoslynかで選択可能にしようとしています。

Monoプロジェクトは今後、.NET Coreプロジェクトと相互に情報交換して、.NET純正のソースコードのMonoへの取り込みや、Monoの相互運用性(Linux対応など)の.NETへの取り込みを行う事を表明しています。また、C#コンパイラのインフラについては、Roslynベースに統一する方針のようです。


スクリプティング・ホスティング

自分で書いた何らかのアプリケーションに、スクリプティング機能を載せたいと思ったことはありませんか? 例えば、何らかのゲームを作っているとして、このゲームにC#のスクリプティング機能を搭載出来たら、アドベンチャーゲームやロールプレイングゲームのシナリオ進行、シミュレーションゲームの敵AI調整に C# のスクリプトが使えるようになるのです。ゲームの世界ではluaやpython辺りをよく聞きますね。でも、本体をC#で書いているなら、スクリプトもC#で書きたいものです。

別の例として、Officeを挙げます。Officeには「Visual Basic for Application」という、VBチックなスクリプト環境があります。が、VBA、書きたくないです… VBAは徐々にフェードアウトの方向になっていると思いますが、代わりにC#でスクリプトが書けたら、もうちょっとOfficeに前向きになりそうです(Excel向けであれば、商用プラグインが既にあります:FCell)。MicrosoftはOfficeでC#を使えるようにするか? は、まだまだ分かりませんが、Roslynが公開されたことで、その可能性は高くなったと思います。

Roslynのコアは、C#のパーサーライブラリです。そして、極めて依存性が低く設計されています(Portable class library化されています)。が、パースしても構文木が得られるだけで、これが実行可能になるわけではありません。この構文木を食わせて、スクリプトとして実行可能にするインフラが「Roslyn for scripting」です。

流れは以下のようになります。

Roslyn Roslyn for scripting
スクリプト(文字列) → 構文木 → IL変換(コンパイル) → 実行

Roslyn for scriptingは、構文木をコンパイルしてIL命令に変換し、更に実行まで行います。IL変換は、System.Reflection.Emitを使用して動的にILを生成します。Emitは環境によって使用出来ない(例:ストアアプリ環境であるWinRTではEmitは使えない)ため、Roslyn本体には含めなかったのだと推察しています。現在のところ、Roslyn for scriptingは.NET4.5以上の環境でのみ使用可能です。

話が複雑になってきましたか? いやいや、Roslyn for scriptingは、ものすごく簡単に使えます!


Hello! C# scripting!!

roslyn1Roslyn for scriptingを使ってスクリプト環境を作る場合、殆どRoslynを意識する必要はありません。とりあえずHello worldをやってみましょう。.NET 4.5以上のコンソールアプリケーションのプロジェクトを作ってください。次に、NuGetで以下のパッケージを導入します。まだ正式版ではないので、「リリース前のパッケージを含める」を選択して下さい。

# もうRCでGoLiveなので、インターフェイスに大きな変更は無いと思われます。
# 最近、NuGetの調子が悪いようなので、上記パッケージが一覧に表示されない場合は、パッケージマネージャコンソールから “Install-Package” コマンドでインストールすると確実です。

導入出来たら、Mainメソッドに以下のようにRoslyn for scriptingの実装を書きます。

using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	class Program
	{
		static void Main(string[] args)
		{
			// Console.WriteLine("Hellow C# scripting!");
			var script = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting!\");");
			script.Run();
		}
	}
}

roslyn2これだけです! さぁ、実行してみましょう!! 簡単ですね! 簡単過ぎて、応用性が無いのでは? と思うかもしれませんが、大丈夫です。追々分かると思います。

ところで、ソースコードの先頭にコメントを入れておきましたが、興味深い事に気が付きましたか?

Consoleクラスは、本来はSystem名前空間に存在しますが、指定していません。これは、事前に「using System;」相当の定義が組み込まれているからです。
また、このコード自体、名前空間の宣言も、クラス宣言も、メソッド宣言もありません。スクリプト環境でこれらを書くことももちろん出来ますが、スクリプト環境ではこれらを省略可能にしているのです。そのため、いきなり実装本体が来ても、全く問題ありません。


スクリプト環境固有の課題

上記のように、スクリプティングの世界では、単にソースコードをリアルタイムで解析・実行するだけではなく、普通のソースコードとは異なる状況に対処する必要があります。以下にそのような課題を挙げます。

  • 記述の省略を可能にする
  • 逐次処理を可能にする
  • ホスト環境と通信する

記述の省略を可能にする

前述のように、スクリプト環境では色々省略して記述出来た方が便利です。ホストする側で、事前にusingを成立させておくことが出来ます。「System」名前空間は既定で定義されていますが、独自の名前空間も定義しておくことが出来ます。そのためには、事前にScriptOptionsクラスを用意して、オプションとして指定します。

using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	class Program
	{
		static void Main(string[] args)
		{
			// using System.Collection.Generic; 相当
			var options = new ScriptOptions().
				WithNamespaces(
					"System.Collections.Generic").
				WithReferences(
					typeof(object).Assembly);

			// var list = new List<string> { "ABC", "DEF" }; System.Console.WriteLine(string.Join(",", list));
			var script1 = CSharpScript.Create(
				"var list = new List<string> { \"ABC\", \"DEF\" }; System.Console.WriteLine(string.Join(\",\", list));",
				options);
			script1.Run();
		}
	}
}

ScriptOptionsや他のクラスもそうですが、Roslynでは「イミュータブルオブジェクト」を全面的に採用しています。イミュータブルオブジェクトとは、文字列(System.String)や日付構造体(System.DateTime)のように、インスタンスの内容が不変である事を保証した実装の事です。一度生成されたインスタンスは、中身が変更されることはありません。

上記の例では「WithNamespaces」メソッドを呼び出していますが、これによって、最初に生成したScriptOptionsクラスのインスタンスを変更するのではなく、新たなScriptOptionsクラスのインスタンスを生成しています。以下がWithNamespacesの定義です。

/// <summary>
/// Creates a new <see cref="T:Microsoft.CodeAnalysis.Scripting.ScriptOptions"/> with the namespaces changed.
/// </summary>
public ScriptOptions WithNamespaces(IEnumerable<string> namespaces)
{
	// (内部実装)
}

このように、WithNamespacesを呼び出すと、名前空間をオプションとして追加した「新しいScriptOptions」を生成し、戻り値として返却します。そのため、単にWithNamespacesを呼び出しただけでは、元のインスタンスには何も変更が加わっていない事になるので注意が必要です。

roslyn3さて、名前空間の定義は「WithNamespaces」メソッドで行いますが、ScriptOptionsの定義を空のインスタンスから始めた(new ScriptOptions())ので、mscorlibへの参照を追加しておく必要があります。それが「WithReferences」メソッドです。

なお、最初の例でオプションを省略しましたが、省略時はmscorlibへの参照と、System名前空間の定義だけが行われた状態になっています。

こうして必要な定義を追加したオプションを生成出来れば、あとは、CSharpScript.Createの引数にオプションを渡すことで、これらの定義が解釈されます。一般的には、「System」「System.Collections.Generic」「System.Linq」「System.Threading.Tasks」などが定義されていた方が、利便性が向上するでしょう。あるいは、これらの定義をホスト側のApp.configや設定ファイルから読み込むようにすれば、更に柔軟性が高くなります。


逐次処理を可能にする

スクリプト環境では、ユーザーがコンソールからインタラクティブにコードを記述する可能性があります。例えば、コマンドプロンプト(cmd.exe)やPowerShellでは、ユーザーがコマンドラインから入力した命令やコードが「Enter」キーの押下で、即実行されます。と言う事は、コンパイル実行の場合は、一度にすべてのコードを評価しなければならないのが、ちょっとづつ、断片的にソースコードが渡される可能性がある事を意味します。この状況を、とりあえず簡単に記述したのが以下の例です。

using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	class Program
	{
		static void Main(string[] args)
		{
			var script1 = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting for first line, and continue...\");");
			script1.Run();

			var script2 = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting for next line!\");");
			script2.Run();
		}
	}
}

単に、CSharpScriptのインスタンスを2回生成して実行しているだけです。これは問題ないでしょう。しかし、スクリプトコードの入力をユーザーがインタラクティブに実行している場合、もっと複雑な問題が発生する可能性があります:

using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	class Program
	{
		static void Main(string[] args)
		{
			var script1 = CSharpScript.Create(
				"var data = 123;");
			script1.Run();

			var script2 = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting with data: {0}\", data);");

			// "(1, 56): error CS0103: The name 'data' does not exist in the current context"
			script2.Run();
		}
	}
}

roslyn4コードは名前空間・クラス・メソッドなどの定義が無くても解釈されるのでしたよね? いきなり変数「data」が定義されていますが、スクリプトとしては問題ありません。問題はscript2です。このスクリプトで、data変数を参照しようとしていますが、コンパイルエラーが発生しています。

これは、script1とscript2が、相互に全く関係のないスクリプトとして実行しているためです。勿論、そのように意図している場合は問題ありませんが、インタラクティブに実行する場合は、このようにコードが分断されてしまう可能性は十分考えられます。

では、どうすれば良いでしょうか? script1で定義されたdata変数をscript2で参照可能にするには、「ScriptState」を使います。

using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	class Program
	{
		static void Main(string[] args)
		{
			var script1 = CSharpScript.Create(
				"var data = 123;");
			var script1State = script1.Run();

			var script2 = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting with data: {0}\", data);");
			script2.Run(script1State);
		}
	}
}

CSharpScript.Runメソッドの戻り値は、ScriptStateです。この中には、スクリプトを実行した結果や、スクリプトとして記憶しておくべき様々な情報が格納されています。これを、次のスクリプト実行時のRunメソッドの引数に渡すと、スクリプトはScriptStateの記憶を前提とした環境で動作します。この中にはdata変数の結果も格納されているので、script2も問題無く実行出来るようになるのです。

さぁ、もう、かなり自分のアプリケーションに組み込める気がしてきましたよね?! 次の問題が片付けば、大抵の環境に組み込んで、すぐに応用が可能ですよ!


ホスト環境と通信する

ここまでで、独立したスクリプトエンジンとして問題なく操作できるようになったはずですが、現実には、スクリプト側のコードから、ホスト環境のオブジェクトを操作したりしたい訳です。でなければ、スクリプト環境を組み込む意味がありませんよね。

前述のCSharpScript.Runメソッドの引数の定義を見て、疑問に思ったかもしれません。

/// <summary>
/// Runs this script.
/// </summary>
/// <param name="globals">An object instance whose members can be accessed by the script as global variables, or a <see cref="T:Microsoft.CodeAnalysis.Scripting.ScriptState"/> instance that was the output from a previously run script.</param>
/// <returns>
/// A <see cref="T:Microsoft.CodeAnalysis.Scripting.ScriptState"/> that represents the state after running the script, including all declared variables and return value.
/// </returns>
public ScriptState Run(object globals = null)
{
	// (内部実装)
}

Runメソッドの引数は「object」です。ここに渡していたのはScriptStateクラスのインスタンスなので、わざわざobjectと定義する意味が分からないかもしれません。実は、この引数にはScriptStateだけではなく、任意のクラスのインスタンスが渡せます。以下に例を示します:

using System;
using System.Linq;

using Microsoft.CodeAnalysis.Scripting.CSharp;

namespace HelloRoslynForScripting
{
	public sealed class HostObject
	{
		public int Value;

		public string CreateValues(int count)
		{
			var r = new Random();
			return string.Join(",", Enumerable.Range(0, count).Select(index => r.Next()));
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			var hostObject = new HostObject { Value = 123 };

			var script1 = CSharpScript.Create(
				"Console.WriteLine(\"Hello C# scripting with host object: Value={0}, Values=[{1}]\", Value, CreateValues(10));");
			script1.Run(hostObject);
		}
	}
}

ホスト側で用意した「HostObject」クラスのインスタンスを渡し、スクリプト側からアクセスしています。面白い事に、スクリプトからはグローバルな無名の要素としてアクセス出来ます。そのため、「Value」フィールドや「CreateValues」メソッドに、インスタンスやクラスの指定がありません。

roslyn5Runの引数に渡せるインスタンスは、クラスである必要があります(構造体はダメです。勿論、クラスのメンバーに構造体があっても問題ありません)。また、クラスはパブリックでなければなりません。恐らく、スクリプトがコンパイルされた際に、クラスのメタデータにアクセスする必要があるからでしょう。


残る課題

ここまで分かれば、後は応用だけです。スクリプト環境として考えられる他の課題は、scriptcsを見てみると分かるかもしれません。

  • インタラクティブな指令で、アセンブリ参照を追加する
  • インタラクティブな指令で、スクリプトをファイルとしてロードする
  • インタラクティブな指令で、NuGetなどのパッケージシステムをサポートする

つまり、インタラクティブなコンソールがコードの入力となる場合は、帯域外の指令で、ホストがスクリプティングのサポートをする必要があります。scriptcsでは、このような独自コマンドを定義(先頭が”:”で始まるコマンドが入力された場合は、スクリプトコードではなく独自に処理を行う。例えば”:load”でスクリプトライブラリをロードする)して対処しています。

さあ、これでC#スクリプトで何でも出来るでしょう!