LoginSignup
9
12

More than 1 year has passed since last update.

CSharpScriptでスクリプトファイル実行機能を実装する

Last updated at Posted at 2021-09-09

はじめに

C#のスクリプト実行用コードが公開されてから6年以上が経ってるんですね.

パッケージ公開からも6年近く(?)ということで, だいぶネット上に日本語の情報が出てきていますが, どこのページも「動かし方」は書いてあっても詳しいところまでは触れられてなさげに感じます(失礼

私自身, ちょいちょい躓いたところがあったので, 改めてまとめて記事にしてみました

ちなみに, Visual Basicのスクリプト実行用実装(VisualBasicScript)も同じタイミングでソースが公開されており, CSharpScriptと同じような感覚で使用することができそうに見えます… が, NuGet.orgではUnlisted Packageになっていますし, まだ機能として使用可能な状態ではないんですかね.


用語解説

私の知識が間違っているかもしれませんが, この投稿では特定の語句の意味を次のように解釈して作成しています.

語句 意味
スクリプト C#で書かれた処理(+ コメント等)で構成された文字列のこと
スクリプト実行 スクリプトに含まれる処理が意味する処理を実行すること
スクリプトファイル スクリプトだけが記録されたファイルのこと

スクリプト実行機能を実装するには

色々なところで触れられていますが, ここにも一応書いておきます

1. nugetパッケージを参照する

プロジェクトにMicrosoft.CodeAnalysis.CSharp.Scriptingパッケージを追加します

最低要件が.NET Standard 2.0なので, .NET Framework4.6.1以上 あるいは .NET Core 2.0以上等で使用できます ( 参考 : Microsoft Docs ".NET Standard" )

依存しているパッケージが色々あるので, パッケージの追加には時間がかかるかもしれません

2. スクリプトを読み込むコードを追加する (CSharpScriptクラス)

実際にスクリプトを読み込む処理は, Microsoft.CodeAnalysis.CSharp.ScriptingnamespaceのCSharpScriptクラスにあるstaticメソッド群を用いて行うのが比較的楽です.
Docsにメソッドの一覧とかあればよかったんですが, 軽く見た感じ無さそう… ということで, 実装のほうから見てみましょう.

こちら, RoslynリポジトリのCSharpScript.csです.
上記リンクは, 記事を書いた時点でmainブランチにあった最新のcommitでの状態を参照しています.

以降も同じタイミングのものを参照していきます.

このファイルにはCSharpScriptクラスが実装されており, partialキーワードが無いのでCSharpScriptクラスのメソッドはこのファイルにしか実装されていないことがわかります.

で, ファイルを見てみると以下のstaticメソッドが含まれていることがわかります.

Script<T> Create<T>(string code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<T> Create<T>(Stream code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<object> Create(string code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)
Script<object> Create(Stream code, ScriptOptions options = null, Type globalsType = null, InteractiveAssemblyLoader assemblyLoader = null)

Task<ScriptState<T>> RunAsync<T>(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))
Task<ScriptState<object>> RunAsync(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))

Task<T> EvaluateAsync<T>(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))
Task<object> EvaluateAsync(string code, ScriptOptions options = null, object globals = null, Type globalsType = null, CancellationToken cancellationToken = default(CancellationToken))

型引数をとらないメソッド(例:Create(...)メソッド)は, 同一名で型引数を取るメソッドの型引数に object を設定(例:Create<object>(...))して呼ぶ実装になっています.

ということは, stringStreamの違いを気にせずに考えると, Createメソッド, RunAsyncメソッド, EvaluateAsyncメソッドの3種類が用意されているわけです.


2.1 Createメソッド

スクリプトを読みこんでオブジェクト化するメソッドです (コンパイルや実行はまだ行われていません)
同じスクリプトを何回も実行する場合はこれが適当です.

Createメソッドは, 型引数を含めて引数を5つ取ります.

  1. T : 型引数. スクリプトの戻り値の型を指定する (デフォルトはobject)
  2. code : 実際のスクリプトのstringあるいはStreamを渡す
  3. options : ScriptOptions型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可)
  4. globalsType : Type型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型
  5. assemblyLoader : InteractiveAssemblyLoader型インスタンスを渡す. 詳細は後述します

戻り値はScript<T>型で, スクリプトを読み取った結果が格納されています.
詳細は後述します.

例として, 2 * 3という計算を行う処理をスクリプトとして入力する場合を考えます.
この場合, 以下のような記述をすれば良いです.

Script<int> createdScript = CSharpScript.Create<int>("return 2 * 3;");

このままでは「スクリプトを読んだ」だけの状態なため, まだ実行結果を受け取ることはできません.

もう少し先で説明しますが, 実行するには上記の例の場合createdScript.RunAsync()ってすればOKです.
戻り値まで取得する場合は, RunAsyncから返ってきたインスタンスのReturnValueプロパティを参照する形になります.

Global Objectを活用するともう少し楽になるので, そちらの説明もご参照ください.

2.2 RunAsyncメソッド

スクリプトを解析してすぐに実行するメソッドです.
あるスクリプトを1回だけ実行すればいいといった場合はこのメソッドが適当です.
RunAsyncメソッドは, 型引数を含めて引数を6つ取ります.

  1. T : 型引数. スクリプトの戻り値の型を指定する (デフォルトはobject)
  2. code : 実際のスクリプトのstringあるいはStreamを渡す
  3. options : ScriptOptions型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可)
  4. globals : object型で, Global Objectとして渡すインスタンスを渡す
  5. globalsType : Type型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型
  6. cancellationToken : CancellationToken型インスタンスを渡す

このRunAsyncメソッド, 内部的にはCreateメソッドを呼んで, 返ってきたScript<T>型インスタンスでRunAsyncメソッドを実行しているだけです.

Script<T>.RunAsyncメソッドについての詳細は後述します.

2.3 EvaluateAsyncメソッド

スクリプトを解析してすぐに実行し, 戻り値を取得するメソッドです.
スクリプト実行後は戻り値さえ取得できればOKといった場合にはこのメソッドが適当です.

引数についてはRunAsyncメソッドと同じなため紹介を省略します.

戻り値はTask<T>型なので, 次のような感じで使用することになります.

int result = await CSharpScript.EvaluateAsync<int>("return 2 * 3;");

このEvaluateAsyncメソッド, 内部ではCSharpScript.RunAsyncメソッドを実行して, 返ってきたTask<ScriptState<T>>型インスタンスのEvaluateAsyncメソッドを実行しているだけ… なんですが, 実はScriptState<T>.EvaluateAsyncメソッドのアクセス修飾子はinternalでして…

とはいえ, ScriptState<T>.EvaluateAsyncメソッドは処理的にはScriptState<T>.ReturnValueを返してるだけなので, そこまで気にする必要はないです.

例えば, このEvaluateAsyncメソッドの代わりにRunAsyncメソッドを利用する場合は, 次のようなコードを書けばOKです.

// resultには 2 * 3 の結果 (= 6) が入る
int result = (await CSharpScript.RunAsync<int>("2 * 3")).ReturnValue;

2.4 Global Objectについて

スクリプトと呼び出し側の間でデータのやり取りを行えるのが Global Object です.

例えば, 次のようなコードを書いたとします.

Sample.cs (50行近くあるので畳んでいます)
Sample.cs
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;
using System.Threading.Tasks;

const string ResultVariableName = "result";

//Create ScriptObject
Script<int>? createdScript =
  CSharpScript.Create<int>(
    // Script
    $"int {ResultVariableName} = Result = A * B; return {ResultVariableName};",
    //ScriptOptions
    null,
    //Type of GlobalObject
    typeof(Hogehoge)
  );

Console.WriteLine("== Compile ==========");

//Compile
var compileResult = createdScript.Compile();

//Print CompileLog (Diagnostics)
foreach (var item in compileResult)
  Console.WriteLine(item);

Console.WriteLine("== Diagnostics Print End ==========");

//Prepare Global Object
Hogehoge globalObject = new() { A = 2, B = 3 };

//Run
var runResult = await createdScript.RunAsync(globalObject);

//Print Result
Console.WriteLine($"globalObject  \t: {globalObject.A} * {globalObject.B} = {globalObject.Result}");
Console.WriteLine($"Var in Script \t: {globalObject.A} * {globalObject.B} = {runResult?.GetVariable(ResultVariableName).Value}");
Console.WriteLine($"From runResult\t: {globalObject.A} * {globalObject.B} = {runResult?.ReturnValue}");

public class Hogehoge
{
  public int A { get; init; }
  public int B { get; init; }
  public int Result { get; set; }
}

C#9.0から追加された "最上位レベルのステートメント" 機能を使っているので, すっきりしているようなしていないような…?

まぁそれはおいといて.

このコードを dotnet run で実行すると, 次のような出力を得ることができます.

output
== Compile ==========
== Diagnostics Print End ==========
globalObject    : 2 * 3 = 6
Var in Script   : 2 * 3 = 6
From runResult  : 2 * 3 = 6

こんな風に, 事前に指定した型のインスタンスを渡すことで, スクリプト内でその型のパブリックメンバにアクセスできます (この書き方はちょっと正確ではないんですが, まぁまぁ)

個人的には "スクリプト内でそのインスタンスに対するメンバーアクセス式を書く際は, 一番最初に出てくる. トークンとその左が要らなくなる…というイメージでいますが, これで伝わるんですかね…?
new Object().Equals(null); でいうと new Object().の部分です

上の説明で理解できた方はわかるかと思いますが, 注意してほしいのは, スクリプトがその型の中で実行されるわけじゃないという点です.

例えば, 先ほどのSample.csのHogehogeクラスを次のように修正してみたとします.

Sample_2.cs
public class Hogehoge
{
  public int A { get; init; }
  internal int B { get; init; }
  public int Result { get; private set; }
}

Bプロパティのアクセス修飾子をinternalにして, Resultプロパティのsetterのアクセス修飾子をprivateに変更してみました.

これを実行すると, 次のようにエラーが出力されます (さすがにUnhandled exceptionのパス出力部分は一部カットしました)

output
== Compile ==========
(1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
(1,27): error CS0103: The name 'B' does not exist in the current context
== Diagnostics Print End ==========
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_2.cs:line 35
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

ということで, スクリプトはクラス/アセンブリの外からアクセスされていることがわかります.

ちなみに, 「スクリプトはクラス/アセンブリの外からアクセスされている」と書いていますが, Global Objectに指定する型のアクセス修飾子もpublicである必要があります.

仮にクラスをprivateにしてしまうと, 次のように怒られます.

output
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0122: 'GlobalObject.A' はアクセスできない保護レベルになっています

ちなみに, 今回はプロパティしか使用していませんが, さきほど "パブリックメンバにアクセスできる" と書いた通り, メソッドやフィールド等にもアクセス可能です.

ちなみにちなみに, 前のほうで

new Object().Equals(null); でいうと new Object().の部分

を省略するイメージと書きましたが, そこを省略しているだけという関係上, 継承元のメソッドも実行できます.
まぁ当たり前と言えば当たり前なんですが.

ものは試しに, 自作のクラスをGlobal Objectに設定したうえで, そのクラスのObject型から継承してきたメソッドをスクリプト内で実行してみようと思います.
すべてのクラスはObject型を継承していますからね.

Sample_3.cs (50行近くあるので畳んでいます)
Sample_3.cs
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;
using System.Threading.Tasks;

const string ResultVariableName = "result";

//Prepare Global Object
Hogehoge globalObject = new() { A = 2, B = 3 };

const string scriptString =
$@"using System;
int {ResultVariableName} = Result = A * B;

Console.WriteLine(Equals(abc));
Console.WriteLine(GetHashCode());
Console.WriteLine(ToString());
Console.WriteLine(GetType());

return {ResultVariableName};
";

//Create ScriptObject
ScriptState<Tint>? runResult =
  await CSharpScript.RunAsync<int>(
    // Script
    scriptString,
    //ScriptOptions
    null,
    //GlobalObject
    globalObject
  );

//Print Result
Console.WriteLine($"globalObject  \t: {globalObject.A} * {globalObject.B} = {globalObject.Result}");
Console.WriteLine($"Var in Script \t: {globalObject.A} * {globalObject.B} = {runResult?.GetVariable(ResultVariableName).Value}");
Console.WriteLine($"From runResult\t: {globalObject.A} * {globalObject.B} = {runResult?.ReturnValue}");

public class Hogehoge
{
  public int A { get; init; }
  public int B { get; init; }
  public int Result { get; set; }

  public static Hogehoge abc = new();
}

Sample_2.csから色々変えてますが, やっていることは同じです.

実行結果は次のようになります

output
False
55467050
Hogehoge
Hogehoge
globalObject    : 2 * 3 = 6
Var in Script   : 2 * 3 = 6
From runResult  : 2 * 3 = 6

正常に実行できていますね

2.5 おまけ : スクリプト内での this キーワード

Microsoft DocsのC#リファレンスには, this キーワードについて, 次のように説明されています.

this キーワードはクラスの現在のインスタンスを参照します。拡張メソッドの最初のパラメーターの修飾子としても使用されます。
(引用元 : this キーワード - C# リファレンス | Microsoft Docs)

では, スクリプトで this キーワードを使って "現在のインスタンス" を取得しようとした場合, 何を得られるのでしょうか? 試しに, 次のようなコードを実行してみます.

Sample_4.cs
await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(
    // Script
    "System.Console.WriteLine(this);",
    //ScriptOptions
    null,
    //GlobalObject
    new System.Object()
  );

さすがにGlobalObjectとして渡したObject型インスタンスを参照するはずもなく.
だからといって新しくクラスが作成されているとも思えず.

実行してみると…

output
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,26): error CS0027: Keyword 'this' is not available in the current context
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync[T](String code, ScriptOptions options, Object globals, Type globalsType, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(String code, ScriptOptions options, Object globals, Type globalsType, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_4.cs:line 1
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

ということで, ここではthisキーワードを使えねぇよって返されることがわかりました.

このことから, スクリプトはstaticメソッドのようなものと考えていいかもしれません.


3. スクリプトを実行するコードを書く (Script/Script<T>クラス)

CSharpScript.RunAsyncメソッドやCSharpScript.EvaluateAsyncメソッドを用いた場合は戻り値としてスクリプトの実行結果が返ってきますが, CSharpScript.Createメソッドを用いた場合はスクリプトを読みこんだ結果しか返してくれません.

ということで, スクリプトを実行するコードを書く必要があります.

簡単な答えを言ってしまうと "ScriptクラスのインスタンスメソッドであるRunAsyncメソッドを実行すればいい" だけですが, もう少し違う使い方もないか調べてみます.

Createメソッドの戻り値はScript<T>型インスタンスです.
なので, ScriptクラスのDocsを確認してみましょう.

ScriptクラスはMicrosoft.CodeAnalysis.Scriptingnamespaceに属しています...が, Microsoft Docsの.NET API browserで検索しても出てきません

ということで, CSharpScriptクラスのときと同じように, roslynリポジトリにあるソースコードからScriptクラスの中身を確認してみます.

こちら, Script.csファイルです
このファイルにはabstractであるScriptクラスと, Scriptクラスを継承したsealedScript<T>クラスが実装されています.

見た感じinternalprivateなメンバが多い印象がありますが, とりあえずScript<T>クラスからpublicかつ実行に関係ありそうなメソッドを抽出してみます.

//Scriptクラスに実装されているメソッドで, Script<T>クラスでoverride等されていないもの
Script<object> ContinueWith(string code, ScriptOptions options = null)
Script<object> ContinueWith(Stream code, ScriptOptions options = null)
Script<TResult> ContinueWith<TResult>(string code, ScriptOptions options = null)
Script<TResult> ContinueWith<TResult>(Stream code, ScriptOptions options = null)
ImmutableArray<Diagnostic> Compile(CancellationToken cancellationToken = default(CancellationToken))

//ここからScript<T>
Task<ScriptState<T>> RunAsync(object globals, CancellationToken cancellationToken)
Task<ScriptState<T>> RunAsync(object globals = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))

ScriptRunner<T> CreateDelegate(CancellationToken cancellationToken = default(CancellationToken))

Task<ScriptState<T>> RunFromAsync(ScriptState previousState, CancellationToken cancellationToken)
Task<ScriptState<T>> RunFromAsync(ScriptState previousState, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))

さて, 色々ありますが…

  • ContinueWith各メソッドについてはそこまで差がないので4つまとめて考えます,
  • RunAsyncRunFromAsyncメソッドのcatchExceptionを取らないやつはcatchExceptionを取るやつのcatchExceptionにnullを入れて呼んでるだけです.

以上のことを頭に入れたうえで, ContinueWith, Compile, RunAsync, CreateDelegate, RunFromAsyncの5つのメソッドについて紹介します.

3.1 ContinueWithメソッド

指定のスクリプトの前に現在のスクリプトを実行するように設定したScript型インスタンスを取得するメソッドです.
インタラクティブな実行環境 / REPLを提供する際はこのメソッドが便利です.

例えば, 2 * 3の計算を実行してResultに入れたうえで, 別のスクリプトでそのResultを2乗するといった場合, 次のようなコードになります.

Sample_5.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;

using System;

const string scriptStr =
    "int A = 2;" +
    "int B = 3;" +
    "int Result = A * B;" +
    "System.Console.WriteLine($\"{A} * {B} = {Result}\");" +
    "return Result;";

Console.WriteLine("== script1 ==");
Microsoft.CodeAnalysis.Scripting.ScriptState<object>? runResult_1 = await CSharpScript.RunAsync(scriptStr);
Console.WriteLine("== script1 complete ==");

var script2 = runResult_1.Script.ContinueWith<int>("Result * Result");

Console.WriteLine("== script2 ==");

Microsoft.CodeAnalysis.Scripting.ScriptState<int>? runResult_2 = await script2.RunAsync();

Console.WriteLine("== script2 complete ==");

Console.WriteLine(runResult_1.ReturnValue);
Console.WriteLine(runResult_2.ReturnValue);

上記のコードを実行すると, 次のような出力を得ることができます.

output
== script1 ==
2 * 3 = 6
== script1 complete ==
== script2 ==
2 * 3 = 6
== script2 complete ==
6
36

このように, script2.RunAsyncを行うと, Result * Resultの前に2 * 3 = 6の計算も行われています.

ちなみに, 後で紹介するRunFromAsyncメソッドを用いると, runResult_1の実行結果を引き継いでResult * Resultの処理を行えます.

3.2 Compileメソッド

スクリプトをコンパイルするメソッドです.
このメソッドを呼ばなくても初回実行時に自動でコンパイルしてくれますが, このメソッドを事前に呼んでおくことで, 実行前に余計な時間をかけずに済みます.

引数であるCancellationToken型のcancellationTokenには, 操作を途中で取り消す際のために使用するトークンを渡します.

…とか書いてますが, 私自身CancellationToken型を使用したことがなく… CancellationTokenについては公式のDocsをご参照ください. サンプルが載ってるのでわかりやすいです.

戻り値はImmutableArray<Diagnostic>型で, コンパイル結果が入っています.
具体的には, Warning以上のコンパイル結果が格納されています.

この指定はハードコーディングされてしまっているので, Compileメソッドの実行でInformationレベルの情報を得ることはできません.
まぁ不要っちゃ不要ではありますが.

で. Issueも開いてますが, このCompileメソッドはコンパイルエラーが起きてもExceptionを吐きません.

エラーが無かったか確認するには, 返ってきたDiagnostic配列にSeverity == DiagnosticSeverity.Errorなインスタンスが含まれていないことを確認する必要があります.

ちなみに, コンパイルでエラーが出たやつをRunAsyncメソッド等で実行しようとすると, そっちでMicrosoft.CodeAnalysis.Scripting.CompilationErrorExceptionがthrowされるので, ご注意ください.
…というか, 「Global Objectについて」のほうでそんな挙動をするって実例を出してましたね.

こっちにも同じものを再掲します.

output
== Compile ==========
(1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
(1,27): error CS0103: The name 'B' does not exist in the current context
== Diagnostics Print End ==========
Unhandled exception. Microsoft.CodeAnalysis.Scripting.CompilationErrorException: (1,14): error CS0200: Property or indexer 'Hogehoge.Result' cannot be assigned to -- it is read only
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.ThrowIfAnyCompilationErrors(DiagnosticBag diagnostics, DiagnosticFormatter formatter)
   at Microsoft.CodeAnalysis.Scripting.ScriptBuilder.CreateExecutor[T](ScriptCompiler compiler, Compilation compilation, Boolean emitDebugInformation, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.GetExecutor(CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunAsync(Object globals, Func`2 catchException, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in /.../Sample_2.cs:line 35
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

== Compile ====って出力の直後にCompileメソッドを呼んで, 返ってきた配列を出力して== Diagnostics Print End ====のあとにRunAsyncメソッドを実行しています (出力の=は一部省略しました)

このように, コンパイルでエラーが起きてもExceptionthrowされないことがわかります.

3.3 RunAsyncメソッド

スクリプトを実行するメソッドです.
スクリプトがコンパイルされていない場合, 自動でコンパイルが行われます.

また, コンパイルされたスクリプトはキャッシュされるので, 次の実行の際にはコンパイル不要です.
もっと言うと別のインスタンスでもキャッシュからコンパイル結果を取ってきてくれたりします(詳細未確認)

RunAsyncメソッドが受ける引数は, 次の3つです.

  1. globals : object型で, Global Objectとして使用するインスタンスを渡します. 詳細は上の方にある"Global Objectについて"をご確認ください
  2. catchException : Func<Exception, bool>型で, スクリプトで発生した例外をcatchするために使用されます. 例外発生時にここで指定したメソッドが呼ばれるわけですが, このメソッドがtrueを返すと「例外処理済み」としてスクリプトの実行が継続されます. 逆に, falseを返すと「例外未処理」としてExceptionが再throwされて, RunAsyncメソッドのところまで投げられます.
  3. cancellationToken : 実行を中断するために使用するトークンです. 詳細はMicrosoft Docsをご参照ください.

このメソッドの戻り値はTask<ScriptState<T>>型で, Task型のWrapを除くと, スクリプト内の変数等の実行結果が含まれています.
詳細は後述します.

3.4 CreateDelegateメソッド

スクリプトを実行してスクリプトの戻り値を返すメソッドを取得するメソッドです.
いや, 正確に言うと, 「スクリプトが意味する処理を実行するScriptRunner<T>delegateを取得する」メソッドなんですが.

ScriptRunner<T>型はここに定義されてまして, 要はGlobalObjectとCancellationTokenを渡してスクリプトの戻り値を受け取る非同期メソッドですね.

何度も処理を実行し, 戻り値さえ取得できればいい」といった場合はこのメソッドでdelegateを取得すると便利です.

コード例を次に示します.

Sample_6.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

Script<int>? script = CSharpScript.Create<int>(
    // Script
    "int Result = Num_0 + Num_1;" +
    "(Num_0, Num_1) = (Num_1, Result);" +
    "return Result;",
    globalsType: typeof(GlobalObject)
  );

ScriptRunner<int> myFunc = script.CreateDelegate();

GlobalObject obj = new();

System.Console.WriteLine(obj.Num_0);
System.Console.WriteLine(obj.Num_1);
for (int i = 0; i < 10; i++)
  System.Console.WriteLine(await myFunc.Invoke(obj));

public class GlobalObject
{
  public int Num_0 { get; set; } = 0;
  public int Num_1 { get; set; } = 1;
}

フィボナッチ数列を出力するプログラムです.
要らないとは思いますが, 出力は次のような感じになります.

output
0
1
1
2
3
5
8
13
21
34
55
89

3.5 RunFromAsyncメソッド

ContinueWithメソッドで連結したスクリプトを, 途中までの実行結果を引き継いで実行する際に使用します.

例として, 次のコードをご覧ください.

Sample_7.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

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

List<ScriptState<object>> scriptResults = new();
List<Script<object>> scripts = new();

scripts.Add(CSharpScript.Create("System.Console.Write(0); return 0;"));

for (int i = 1; i < 10; i++)
    scripts.Add(scripts[i - 1].ContinueWith($"System.Console.Write({i}); return {i};"));

foreach (var i in scripts)
{
    scriptResults.Add(await i.RunAsync());
    Console.WriteLine();
}

Console.WriteLine("=== Prepare Complete ===");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 2, 5)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 0, 9)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 8, 9)).ReturnValue}");
Console.WriteLine($"\treturn : {(await RunFromTo(scriptResults, 8, 8)).ReturnValue}");

static Task<ScriptState> RunFromTo(List<ScriptState<object>> scriptResults, int from, int to)
{
    Console.WriteLine($"\n====== from:{from}, to:{to}");
    return scriptResults[to].Script.RunFromAsync(scriptResults[from]);
}

このコードを実行すると, 次のような出力を得ることができます.

output
0
01
012
0123
01234
012345
0123456
01234567
012345678
0123456789
=== Prepare Complete ===

====== from:2, to:5
345     return : 5

====== from:0, to:9
123456789       return : 9

====== from:8, to:9
9       return : 9

====== from:8, to:8
        return : 8

このように, ContinueWithメソッドで前に連結したスクリプトを実行する場合, RunAsyncメソッドで行うと一番初めのスクリプトから実行されてしまうのに対して, RunFromAsyncで任意の段階のスクリプト実行結果とともに実行してあげると, そこまでのスクリプトは実行せず次の段階から実行してくれます.

例えば, 上の出力でfrom:2, to:5という出力がありますが, これは「scripts[2]までの実行結果をもとに, script[5]までを順番に実行する」処理を行うことを意味しており, 実際にscript[2]以前の出力が無くscript[3]からscript[5]までしか実行されていないことがわかります.

ちなみに, Script<T>.ContinueWith(...).RunFromAsync(...)をまとめたメソッドがありまして, それが後程紹介するScriptState<T>.ContinueWithAsyncメソッドです.


4. 実行結果を取り出すコードを書く (ScriptState/ScriptState<T>クラス)

利用したメソッドによってはダイレクトに戻り値を取得できているかもしれませんが, とりあえずここではCSharpScript.RunAsyncメソッドや, Script<T>.RunAsyncメソッド等の戻り値として得られるScriptState<T>型から, 目当ての情報を取得する方法を紹介します.

このクラスもDocsがなさげなので, 実装を読んで紹介します.

ScriptState<T>型は同じScriptState.csファイル内に実装されたScriptクラスを継承したクラスです.
基本的にScript<T>クラスで独自に実装されている機能は無いので, Scriptクラスに実装されたpublicメンバに注目します.

Scriptクラスには, 次の9個のpublicメンバが含まれています(objectクラスから継承したメソッドを除く)

Script Script { get; }
Exception Exception { get; }
object ReturnValue { get; }
ImmutableArray<ScriptVariable> Variables { get; }
ScriptVariable GetVariable(string name)

Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options, CancellationToken cancellationToken)
Task<ScriptState<object>> ContinueWithAsync(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))
Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options, CancellationToken cancellationToken)
Task<ScriptState<TResult>> ContinueWithAsync<TResult>(string code, ScriptOptions options = null, Func<Exception, bool> catchException = null, CancellationToken cancellationToken = default(CancellationToken))

Script<T>クラスではReturnValueプロパティがT型だったりしますが, まぁそれは置いといて…

4.1 Scriptプロパティ

このScriptState型インスタンスに対応するScript型インスタンスです.

言葉だけで説明されても…って方は, 次のコードを読むと理解できると思います.

Sample_8.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

Script<object>? script = CSharpScript.Create("int.MaxValue");

ScriptState<object>? result_1 = await script.RunAsync();
System.Console.WriteLine(Equals(script, result_1.Script)); //True

ScriptState<object>? result_2 = await result_1.ContinueWithAsync("int.MinValue");
System.Console.WriteLine(Equals(script, result_2.Script)); //False

ScriptState<object>? result_3 = await CSharpScript.RunAsync("int.MaxValue");
System.Console.WriteLine(Equals(script, result_3.Script)); //False

4.2 Exceptionプロパティ

スクリプト内でthrowされた例外です.
throwされなければnull (正確にはdefaultだけど, 結果的にnullなのには変わりなし) が入ってます.

以下にサンプルコードを示します.

Sample_9.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;

try
{
    Script<object>? script = CSharpScript.Create("throw new System.Exception(\"Exception from Script\");");

    ScriptState<object>? result = await script.RunAsync(catchException: ex =>
    {
        System.Console.WriteLine($"~~~catchException~~~~~\n{ex}");
        return true;
    });

    Console.WriteLine("~~~result.Exception~~~~~");
    Console.WriteLine(result?.Exception.ToString() ?? "null");
}
catch(Exception ex)
{
    Console.WriteLine("~~~caught Exception~~~~~");
    Console.WriteLine(ex);
}

このコードの実行結果は次のとおりです.

output
~~~catchException~~~~~
System.Exception: Exception from Script
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
~~~result.Exception~~~~~
System.Exception: Exception from Script
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)

一旦catchExceptionに指定した処理でExceptionの発生を受け取ったものの, trueを返して「処理済み」扱いとしたので処理が継続されています.
そのため, 無事に実行が完了し, result.Exceptionを参照することに成功しています.

4.3 ReturnValueプロパティ

スクリプトの戻り値です.
スクリプトに戻り値を返すコードが無かった場合, default(T)が記録されています.

以下にサンプルコードを示します.

Sample_10.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;
using System.Threading.Tasks;

ScriptState<object>[]? results = await Task.WhenAll(
    CSharpScript.RunAsync("1.2 * 2.3"),
    CSharpScript.RunAsync("2 * 3"),
    CSharpScript.RunAsync("new System.Object()"),
    CSharpScript.RunAsync("object obj;"),
    CSharpScript.RunAsync("int A = 0;")
);

ScriptState<int>? result_1 = await CSharpScript.RunAsync<int>("int A = 0;");


foreach (var i in results)
    Console.WriteLine($"{i.ReturnValue?.GetType()?.ToString() ?? "null"} : {i.ReturnValue?.ToString() ?? "null"}");

Console.WriteLine($"{result_1.ReturnValue.GetType()?.ToString() ?? "null"} : {result_1.ReturnValue.ToString() ?? "null"}");

上記コードの実行結果は次のとおりです.

output
System.Double : 2.76
System.Int32 : 6
System.Object : System.Object
null : null
null : null
System.Int32 : 0

4.4 Variablesプロパティ

スクリプト内のフィールドが記録された配列です.

以下の条件に合致するフィールドだけを取得できます.

  • publicである
    • とはいえ, スクリプト内でprivateなフィールドを作成することはできませんが
  • フィールド名の長さが0よりも大きい
    • とはいえ, フィールド名の長さを0にすることなんでできないとは思いますが
  • フィールド名が「文字, 数字, _(アンダーバー)」のいずれかで始まっている
    • 自動で生成された(<とか>とかそういうのを含む)フィールドは取得できません.
    • 「文字, 数字」の判定はchar.IsLetterOrDigit(char)メソッドを利用して行われているので, 正確には「UnicodeカテゴリでUppercaseLetter, LowercaseLetter, TitlecaseLetter, ModifierLetter, OtherLetter, DecimalDigitNumberのいずれかに属する文字」+「_(アンダーバー)」だったりします (参照 : Char.IsLetterOrDigit Method (System) | Microsoft Docs)

まぁ, 普通の使い方(?)をしていれば, スクリプト内で手動で定義したフィールドを取得できないなんてことはないと思います.

以下にサンプルコードを示します.

Sample_11.cs
const string scriptString =
    "int A = 123;" +
    "double B = 1.23;" +
    "static object obj = null;" +
    "const float constFloat = 2.34F;" +
    "readonly byte readonlyByte = 0x12;" +
    "short getonlyShort { get; } = 0x123;" +
    "string ABC { get; set; } = \"1 a 2 s 3\";" +
    "int fromShort = getonlyShort * 2;" +
    "private int @private = 999;";

var result = await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(scriptString);

foreach (var i in result.Variables)
    System.Console.WriteLine($"{nameof(i.IsReadOnly)}:{i.IsReadOnly},\t{nameof(i.Name)}:{i.Name},\t{nameof(i.Type)}:{i.Type},\t{nameof(i.Value)}:{i.Value}");

出力は次のとおりです.

output
IsReadOnly:False,       Name:A, Type:System.Int32,      Value:123
IsReadOnly:False,       Name:B, Type:System.Double,     Value:1.23
IsReadOnly:True,        Name:readonlyByte,      Type:System.Byte,       Value:18
IsReadOnly:False,       Name:fromShort, Type:System.Int32,      Value:582
IsReadOnly:False,       Name:private,   Type:System.Int32,      Value:999
IsReadOnly:False,       Name:obj,       Type:System.Object,     Value:
IsReadOnly:True,        Name:constFloat,        Type:System.Single,     Value:2.34

まとめると, こんな感じです.

  • プロパティは取得できない
  • readonlyキーワードやconstキーワードを付けると, ScriptVariable.IsReadOnlyTrueになる
  • 基本は宣言した順に入る. ただし, staticconstキーワードを付けたフィールドについては末尾にstaticconstの順で入れられる

4.5 GetVariableメソッド

Variableプロパティで取得できるフィールド配列から, 特定のフィールド名を持つものを取得するメソッドです.
取得に失敗した場合はnullが返ります.

サンプルコードは省略します. さすがにわかるよね…?

4.6 ContinueWithAsyncメソッド

「実行結果の取得」ではないですが, 同じScriptStateクラスに含まれているので, ここで紹介します.

このメソッドは, 現在の状態を保持して指定のスクリプトを実行し, 結果を取得するメソッドです.
インタラクティブな実行環境を提供したい場合に最適です.

例えば, Sample_6.csの「フィボナッチ数列を生成する処理」をGlobal Objectを使用せずに書いてみます.

Sample_12.cs
using System;

using Microsoft.CodeAnalysis.CSharp.Scripting;

var result = await CSharpScript.RunAsync("int Num_0 = 0; int Num_1 = 1; int Result = Num_0 + Num_1; return Result;");

Console.WriteLine(result.GetVariable("Num_0").Value);
Console.WriteLine(result.GetVariable("Num_1").Value);
Console.WriteLine(result.ReturnValue);

for (int i = 1; i < 10; i++)
{
    result = await result.ContinueWithAsync("(Num_0, Num_1) = (Num_1, Result); Result = Num_0 + Num_1; return Result;");
    Console.WriteLine(result.ReturnValue);
}

このコードを実行すると, 次のようにフィボナッチ数列を得ることができます.

output
0
1
1
2
3
5
8
13
21
34
55
89

スクリプト実行機能の各種オプションを弄る (ScriptOptionsクラス)

ここまでで, おそらく「スクリプトを読んで, 実行する実装」はできたのではないかと思います.

で. ここからは, 自身の目的に最適なオプションを選択するために, ScriptOptionsクラスおよびそこで使用する各種クラスを見ていきます.

ScriptOptions.DefaultCSharpScript.Createメソッド等でoptionsnullを指定した際に使用されます.

ではでは.
パブリックメンバを列挙してみます.

static ScriptOptions Default { get; }

ImmutableArray<MetadataReference> MetadataReferences { get; }
MetadataReferenceResolver MetadataResolver { get; }
SourceReferenceResolver SourceResolver { get; }
ImmutableArray<string> Imports { get; }
bool EmitDebugInformation { get; }
Encoding FileEncoding { get; }
string FilePath { get; }
OptimizationLevel OptimizationLevel { get; }
bool CheckOverflow { get; }
bool AllowUnsafe { get; }
int WarningLevel { get; }

ScriptOptions WithFilePath(string filePath)

ScriptOptions WithReferences(IEnumerable<MetadataReference> references)
ScriptOptions WithReferences(params MetadataReference[] references)
ScriptOptions AddReferences(IEnumerable<MetadataReference> references)
ScriptOptions AddReferences(params MetadataReference[] references)
ScriptOptions WithReferences(IEnumerable<Assembly> references)
ScriptOptions WithReferences(params Assembly[] references)
ScriptOptions AddReferences(IEnumerable<Assembly> references)
ScriptOptions AddReferences(params Assembly[] references)
ScriptOptions WithReferences(IEnumerable<string> references)
ScriptOptions WithReferences(params string[] references)
ScriptOptions AddReferences(IEnumerable<string> references)
ScriptOptions AddReferences(params string[] references)

ScriptOptions WithMetadataResolver(MetadataReferenceResolver resolver)
ScriptOptions WithSourceResolver(SourceReferenceResolver resolver)

ScriptOptions WithImports(IEnumerable<string> imports)
ScriptOptions WithImports(params string[] imports)
ScriptOptions AddImports(IEnumerable<string> imports)
ScriptOptions AddImports(params string[] imports)

ScriptOptions WithEmitDebugInformation(bool emitDebugInformation)
ScriptOptions WithFileEncoding(Encoding encoding)
ScriptOptions WithOptimizationLevel(OptimizationLevel optimizationLevel)
ScriptOptions WithAllowUnsafe(bool allowUnsafe)
ScriptOptions WithCheckOverflow(bool checkOverflow)
ScriptOptions WithWarningLevel(int warningLevel)

recordがほしくなるメソッド群ですね.
このほかにScriptOptionsExtensionsクラスにWithLanguageVersionメソッドが実装されているので, そちらについても紹介します.

基本的な使い方は, ScriptOptions.Default.WithAllowUnsafe(true).AddInports("System")みたいな感じで, ScriptOptions.DefaultWith~~~メソッドやAdd~~~メソッドをつらつら連ねていく感じになります.

さすがにメソッドに説明は要らないでしょうから, 各プロパティの意味だけ書いておきます.

1. MetadataReferencesプロパティ

スクリプト内でデフォルトで (#rディレクティブなしに) 使用できるアセンブリ群です.

デフォルトでは25個のアセンブリがリストされてます.

とはいえ, ここで指定されたstringはUnresolvedMetadataReferenceクラスのコンストラクタに渡されて, その渡されたstringが記録されるだけなので, この時点でアセンブリが読み込まれているわけではありません.

With~~~メソッドやらAdd~~~メソッドやら, 引数の型の違いで色々ありますが…

2. MetadataResolverプロパティ

MetadataReferenceResolverクラスは, 簡単に言うと, 参照するDLLやEXEファイルを探すためのクラスです.
通常はデフォルトのままで十分だと思われます.

最終的にMicrosoft.CodeAnalysis.CSharpnamespaceのCSharpCompilationOptionsクラス… というよりかはその基底クラスのMicrosoft.CodeAnalysisnamespaceのCompilationOptionsクラスのMetadataReferenceResolverプロパティにコピーされて使用されるわけですが… どこでforeachで回されることになるReferenceDirectivesが作成されているかつかめず…

MetadataReferenceResolverクラスのXMLコメントによると#rディレクティブによって呼ばれるとありますが, ScriptOptionsクラスのMetadataResolverプロパティのXMLコメントには依存関係の解決に失敗したときや参照が未解決なものにも呼ばれるっぽいことが書いてあります(以下の引用文参照).

to be used to resolve missing dependencies, unresolved metadata references and #r directives.

(DeepL使用 参考訳) 見つからない依存関係、未解決のメタデータ参照、#rディレクティブを解決するために使用されます

まぁでも, 使ってみた感じmissing dependenciesのresolveにも使われてそうな感じがします.

デフォルトではRuntimeMetadataReferenceResolverが指定されています.

[2021年9月11日追記] 完全にScriptMetadataResolverクラスを見落としていました… RuntimeMetadataReferenceResolverScriptMetadataResolverを介して触ることになります 詳細は末尾に追記します

なお, このクラスはinternalなので, 普通の方法ではインターフェイス経由でしか触れません.

MetadataReferenceResolverクラスabstractなので, デフォルトのものか, あるいは自分で継承して使用することになります.

継承して作成する際の注意事項等は後述します

3. SourceResolverプロパティ

SourceReferenceResolverクラスは, #loadディレクティブで指定されたファイルを探すためのクラスです.

これは本当に#loadディレクティブによるファイルロード以外には使われてなさげです.

デフォルトではSourceReferenceResolverクラスが指定されています.
こっちはRuntimeMetadataReferenceResolverとは違ってpublicなので便利です.

SourceReferenceResolverのコンストラクタは次のような3種の引数構成をとります.

SourceFileResolver(IEnumerable<string> searchPaths, string? baseDirectory)
SourceFileResolver(ImmutableArray<string> searchPaths, string? baseDirectory)
SourceFileResolver(ImmutableArray<string> searchPaths, string? baseDirectory, ImmutableArray<KeyValuePair<string, string>> pathMap)
  • searchPaths : ここで指定されたディレクトリからファイルが検索されます
    • 但し, baseDirectoryが指定されていた場合は, そこから先に検索されます
    • 相対パスでも構いませんが, CurrentWorkingDirectoryからの相対パスになる点にご注意ください.
  • baseDirectory : ファイルを検索する起点とするディレクトリを指定します
    • 絶対パスを指定する必要があります
  • pathMap : パスの先頭を置換する際のパターンを指定します.

#loadディレクティブで相対パスを指定しても, デフォルトのままだとCurrentWorkingDirectoryからの相対パスとして解釈されてしまうため, baseDirectoryをスクリプトファイルが存在するディレクトリにセットしてあげる必要があります.
あるいは, ScriptOptions.FilePathにロード元のスクリプトファイルのパスを指定しても大丈夫です.

4. Importsプロパティ

C#10.0でいうglobalなusingディレクティブです.
対象のスクリプトファイルで, 指定したusingが不要になります.

例えば, 次のようなコードを実行できます.

Sample_13.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

//通常はSystem.Console.WriteLine(...)にする必要がある
_ = await CSharpScript.RunAsync("Console.WriteLine(\"Hello World!\")", ScriptOptions.Default.WithImports("System"));

5. EmitDebugInformationプロパティ

デバッグ用の情報を含めるかどうかです.
デフォルトではfalseなので, デバッグ用情報(pdb的なの)は含まれません.

FilePathプロパティでスクリプトファイルへのパスを指定しないと意味が無いので, ご注意ください.

6. FileEncodingプロパティ

スクリプトをデバッグする際に使用します.

nullを指定するとエンコーディングが自動で検出されます.

7. FilePathプロパティ

スクリプトがファイルから読み込まれている場合に, スクリプトファイルへのパスをセットします.

EmitDebugInformationtrueにしたうえで, 必要に応じてFileEncodingを設定し, このFilePathプロパティにスクリプトファイルへのパスを指定することで, スクリプトのデバッグが可能になります.

ちなみに, stringCreateする場合はFileEncodingの指定が必須になりますので, ご注意ください.

Visual Studio 2022 PreviewでC#スクリプトのデバッグをしている様子

stringで渡す際のサンプルを次に示します.

Sample_14.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System.Text;

const string filePath = @"C:\Users\tetsu\source\repos\TR.Practice.CSharpScript\TR.Practice.CSharpScript\sample.csx";

var scriptFile = System.IO.File.ReadAllText(filePath, Encoding.UTF8);

_ = await CSharpScript.RunAsync(
    scriptFile,
    ScriptOptions.Default
        .WithFilePath(filePath)
        .WithFileEncoding(Encoding.UTF8)
        .WithEmitDebugInformation(true)
    );

8. OptimizationLevelプロパティ

スクリプトからILコードに変換する際の最適化レベルです.
Debug OR Releaseを選択できます.

デフォルトはDebugです.

Releaseにすると(当たり前ではありますが)デバッグができなくなる場合があるので, ご注意ください.

9. CheckOverflowプロパティ

整数演算のオーバーフローチェックを行うかどうかです.

以下に例を示します.

Sample_15.cs
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;

using System;

const string scriptFile = "int A = int.MaxValue; A /= 2; return (A + 1) * 2;";

Console.WriteLine("=== result_1 ===");

var result_1 = await CSharpScript.RunAsync(scriptFile, ScriptOptions.Default.WithCheckOverflow(false));

Console.WriteLine(result_1.ReturnValue);

Console.WriteLine("=== result_2 ===");

var result_2 = await CSharpScript.RunAsync(scriptFile, ScriptOptions.Default.WithCheckOverflow(true));

Console.WriteLine(result_2.ReturnValue);

intの最大値を2で割って1を足してから2倍にしているのでオーバーフローは確実なんですが, 出力を見ればわかるように, CheckOverflowfalseにしておくとExceptionがthrowされません.

output
=== result_1 ===
-2147483648
=== result_2 ===
Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.
   at Submission#0.<<Initialize>>d__0.MoveNext()
--- End of stack trace from previous location ---
   at Microsoft.CodeAnalysis.Scripting.ScriptExecutionState.RunSubmissionsAsync[TResult](ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, StrongBox`1 exceptionHolderOpt, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
   at Microsoft.CodeAnalysis.Scripting.Script`1.RunSubmissionsAsync(ScriptExecutionState executionState, ImmutableArray`1 precedingExecutors, Func`2 currentExecutor, Func`2 catchExceptionOpt, CancellationToken cancellationToken)
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\Users\tetsu\source\repos\TR.Practice.CSharpScript\TR.Practice.CSharpScript\Sample.cs:line 16
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

10. AllowUnsafeプロパティ

unsafeキーワードの使用を許可するかどうかです.

正確に言えばunsafeコンテキストを許可するかどうかでしょうけど, まぁまぁ.

11. WarningLevelプロパティ

表示する警告(Warning)メッセージのレベルを指定します.

レベルの数値についてはC#コンパイラオプションと同じだと思われるので, そちらをご参照ください.

XMLコメントには0以上4以下と書いてありますが, コンパイラと同じようにC#9.0以上では5も有効かもしれません.

9999等を入れてもエラーは吐かないので, 将来のために9999等の大きな数字を入れておいてもいいかもしれません.

意図的にWarningを出す#warningディレクティブがスクリプトでは効かなかったので, 実行例は省略します.

#errorは効くのに, なんででしょうね?

12. WithLanguageVersionメソッド

拡張メソッドで, 言語バージョンを指定できます.
指定できるLanguageVersionはMicrosoft.CodeAnalysis.CSharp.LanguageVersionに列挙されていますが, Microsoft.CodeAnalysis.CSharpパッケージのVersion 3.11.0ではCSharp10が存在しないので, ご注意ください (まぁまだ正式リリースされてないので当たり前ではありますが)

言語バージョン設定は各プロパティがinternalなので, セット後は外から参照できません. ご注意ください.

独自のMetadataReferenceResolverをつくる (MetadataReferenceResolverクラス)

デフォルトのMetadataReferenceResolverであるRuntimeMetadataReferenceResolverinternalなクラスなので, デフォルトでは指定されていないbaseDirectoryを指定したインスタンスを作って使う…ということは難しいです.

[2021年9月11日追記] baseDirectory等はScriptMetadataResolverを介して触ることができました… とはいえ, NuGetサポートはないので, それを求める場合は自力で実装することになります.

ということで, 独自のMetadataReferenceResolverを作ってみましょう.

MyMetadataReferenceResolver.cs
using Microsoft.CodeAnalysis;

using System;
using System.Collections.Immutable;

namespace TR.Practice
{
    public class MyMetadataReferenceResolver : MetadataReferenceResolver
    {
        public override bool Equals(object? other) => throw new NotImplementedException();

        public override int GetHashCode() => throw new NotImplementedException();

        public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties)
            => throw new NotImplementedException();
    }
}

初期状態はこんな感じです.
RuntimeMetadataReferenceResolverではGACとかからアセンブリを検索するコードが書かれていますが, それを再実装するのは手間なので, ScriptOpions.Default.MetadataResolver経由で資源を再利用しちゃいましょう.

MyMetadataReferenceResolver.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Scripting;

using System.Collections.Immutable;

namespace TR.Practice
{
    public class MyMetadataReferenceResolver : MetadataReferenceResolver
    {
        MetadataReferenceResolver Resolver { get; } = ScriptOptions.Default.MetadataResolver;
        public override bool Equals(object? other) => Resolver.Equals(other);

        public override int GetHashCode() => Resolver.GetHashCode();

        public override ImmutableArray<PortableExecutableReference> ResolveReference(string reference, string? baseFilePath, MetadataReferenceProperties properties)
        {
            var baseResult = Resolver.ResolveReference(reference, baseFilePath, properties);
            if (!baseResult.IsDefaultOrEmpty)
                return baseResult;

            //独自の処理を書く

            return ImmutableArray<PortableExecutableReference>.Empty;
        }

        public override bool ResolveMissingAssemblies => Resolver.ResolveMissingAssemblies;

        public override PortableExecutableReference? ResolveMissingAssembly(MetadataReference definition, AssemblyIdentity referenceIdentity)
            => Resolver.ResolveMissingAssembly(definition, referenceIdentity);

        public override string ToString() => Resolver.ToString() ?? string.Empty;

    }
}

で, ResolveReferenceメソッドに置いた//独自の処理を書くってところに独自の処理を書けばOKです.

ちなみに, ResolveReferenceメソッドの引数について…

  • referenceには#rディレクティブで書いた文字列が渡されます
  • baseFilePathにはScriptOptions.FilePathが渡されます

で. F# Interactiveでは#r "nuget:Newtonsoft.Json"みたいに書くことでnugetからパッケージを落とせたり, #iディレクティブでパッケージソースを追加できたりできるそうですが, RuntimeMetadataReferenceResolverには実装されていません (というか, CSharpScript自体#iディレクティブをサポートしてなかったりしますが)

一応abstractNuGetPackageResolverクラスが定義され, RuntimeMetadataReferenceResolverから使用されるような実装になってたりしますが, 現時点でこれを実装したクラスはroslynプロジェクト内になく, またそもそもRuntimeMetadataReferenceResolver自体internalなのでNuGetPackageResolverクラスを継承させたのをセットすることもできません.


CSharpScript.CreateメソッドのassemblyLoader

CSharpScript.Createメソッドに限っては, InteractiveAssemblyLoaderの引数assemblyLoaderを取るので, そちらでアセンブリのロードを制御することも可能です.

InteractiveAssemblyLoader(MetadataShadowCopyProvider shadowCopyProvider = null)

InteractiveAssemblyLoaderクラスのコンストラクタはMetadataShadowCopyProviderクラスインスタンスをとります.

MetadataShadowCopyProvider(string directory = null, IEnumerable<string> noShadowCopyDirectories = null, CultureInfo documentationCommentsCulture = null)

このdirectoryに相対パスの起点とするディレクトリへのパスを指定することで, そこにあるアセンブリの読み込みが可能になります.


さいごに

気づいたらすごい長くなりました.

たぶん色々間違っているところがあると思うので, 何かありましたらコメント等でご指摘いただけると嬉しいです.


2021年9月11日追記 ScriptMetadataResolverクラスについて

コードを読み落としてまして, デフォルトのMetadataReferenceResolverにはScriptMetadataResolverクラスが使用されていました.

RuntimeMetadataReferenceResolverScriptMetadataResolverから触ることになります.

ScriptMetadataResolver WithSearchPaths(params string[] searchPaths)
ScriptMetadataResolver WithSearchPaths(IEnumerable<string> searchPaths)
ScriptMetadataResolver WithSearchPaths(ImmutableArray<string> searchPaths)

ScriptMetadataResolver WithBaseDirectory(string? baseDirectory)

プロパティを変更する系のメソッドはこれだけです.

WithSerchPathsメソッドはRelativePathResolverクラスに渡すパス群を変更するメソッドです.
アセンブリを探すディレクトリの完全修飾パス(絶対パス)を指定します

WithBaseDirectoryメソッドは, SourceReferenceResolverクラスと同じくアセンブリ検索の起点となるディレクトリです.

9
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
12