はじめに
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.Scripting
namespaceの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>(...)
)して呼ぶ実装になっています.
ということは, string
とStream
の違いを気にせずに考えると, Create
メソッド, RunAsync
メソッド, EvaluateAsync
メソッドの3種類が用意されているわけです.
2.1 Create
メソッド
スクリプトを読みこんでオブジェクト化するメソッドです (コンパイルや実行はまだ行われていません)
同じスクリプトを何回も実行する場合はこれが適当です.
各Create
メソッドは, 型引数を含めて引数を5つ取ります.
-
T
: 型引数. スクリプトの戻り値の型を指定する (デフォルトはobject
) -
code
: 実際のスクリプトのstring
あるいはStream
を渡す -
options
:ScriptOptions
型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可) -
globalsType
:Type
型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型 -
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つ取ります.
-
T
: 型引数. スクリプトの戻り値の型を指定する (デフォルトはobject
) -
code
: 実際のスクリプトのstring
あるいはStream
を渡す -
options
:ScriptOptions
型で, スクリプトを解釈するオプション(コンパイルオプションのようなものと考えてOK)を指定できる (省略可) -
globals
:object
型で, Global Objectとして渡すインスタンスを渡す -
globalsType
:Type
型で, スクリプトにGlobal Object (Cとかでいう大域変数的な) として渡すオブジェクトの型 -
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行近くあるので畳んでいます)
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
で実行すると, 次のような出力を得ることができます.
== 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
クラスを次のように修正してみたとします.
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のパス出力部分は一部カットしました)
== 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にしてしまうと, 次のように怒られます.
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行近くあるので畳んでいます)
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から色々変えてますが, やっていることは同じです.
実行結果は次のようになります
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
キーワードを使って "現在のインスタンス" を取得しようとした場合, 何を得られるのでしょうか? 試しに, 次のようなコードを実行してみます.
await Microsoft.CodeAnalysis.CSharp.Scripting.CSharpScript.RunAsync(
// Script
"System.Console.WriteLine(this);",
//ScriptOptions
null,
//GlobalObject
new System.Object()
);
さすがにGlobalObjectとして渡したObject
型インスタンスを参照するはずもなく.
だからといって新しくクラスが作成されているとも思えず.
実行してみると…
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.Scripting
namespaceに属しています...が, Microsoft Docsの.NET API browserで検索しても出てきません
ということで, CSharpScriptクラスのときと同じように, roslynリポジトリにあるソースコードからScript
クラスの中身を確認してみます.
こちら, Script.csファイルです
このファイルにはabstract
であるScript
クラスと, Script
クラスを継承したsealed
なScript<T>
クラスが実装されています.
見た感じinternal
やprivate
なメンバが多い印象がありますが, とりあえず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つまとめて考えます, -
RunAsync
とRunFromAsync
メソッドのcatchException
を取らないやつはcatchException
を取るやつのcatchException
にnullを入れて呼んでるだけです.
以上のことを頭に入れたうえで, ContinueWith
, Compile
, RunAsync
, CreateDelegate
, RunFromAsync
の5つのメソッドについて紹介します.
3.1 ContinueWith
メソッド
指定のスクリプトの前に現在のスクリプトを実行するように設定したScript
型インスタンスを取得するメソッドです.
インタラクティブな実行環境 / REPLを提供する際はこのメソッドが便利です.
例えば, 2 * 3
の計算を実行してResult
に入れたうえで, 別のスクリプトでそのResult
を2乗するといった場合, 次のようなコードになります.
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);
上記のコードを実行すると, 次のような出力を得ることができます.
== 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をご参照ください. サンプルが載ってるのでわかりやすいです.
https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken
戻り値はImmutableArray<Diagnostic>
型で, コンパイル結果が入っています.
具体的には, Warning以上のコンパイル結果が格納されています.
この指定はハードコーディングされてしまっているので, Compile
メソッドの実行でInformationレベルの情報を得ることはできません.
まぁ不要っちゃ不要ではありますが.
で. Issueも開いてますが, このCompileメソッドはコンパイルエラーが起きてもExceptionを吐きません.
エラーが無かったか確認するには, 返ってきたDiagnostic
配列に**Severity == DiagnosticSeverity.Error
なインスタンスが含まれていないことを確認**する必要があります.
https://github.com/dotnet/roslyn/issues/5949
ちなみに, コンパイルでエラーが出たやつをRunAsync
メソッド等で実行しようとすると, そっちでMicrosoft.CodeAnalysis.Scripting.CompilationErrorException
がthrowされるので, ご注意ください.
…というか, 「Global Objectについて」のほうでそんな挙動をするって実例を出してましたね.
こっちにも同じものを再掲します.
== 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
メソッドを実行しています (出力の=
は一部省略しました)
このように, コンパイルでエラーが起きてもException
はthrow
されないことがわかります.
3.3 RunAsync
メソッド
スクリプトを実行するメソッドです.
スクリプトがコンパイルされていない場合, 自動でコンパイルが行われます.
また, コンパイルされたスクリプトはキャッシュされるので, 次の実行の際にはコンパイル不要です.
もっと言うと別のインスタンスでもキャッシュからコンパイル結果を取ってきてくれたりします(詳細未確認)
RunAsync
メソッドが受ける引数は, 次の3つです.
-
globals
:object
型で, Global Objectとして使用するインスタンスを渡します. 詳細は上の方にある"Global Objectについて"をご確認ください -
catchException
:Func<Exception, bool>
型で, スクリプトで発生した例外をcatchするために使用されます. 例外発生時にここで指定したメソッドが呼ばれるわけですが, このメソッドがtrue
を返すと「例外処理済み」としてスクリプトの実行が継続されます. 逆に,false
を返すと「例外未処理」としてExceptionが再throwされて,RunAsync
メソッドのところまで投げられます. -
cancellationToken
: 実行を中断するために使用するトークンです. 詳細はMicrosoft Docsをご参照ください.
このメソッドの戻り値はTask<ScriptState<T>>
型で, Task
型のWrapを除くと, スクリプト内の変数等の実行結果が含まれています.
詳細は後述します.
3.4 CreateDelegate
メソッド
スクリプトを実行してスクリプトの戻り値を返すメソッドを取得するメソッドです.
いや, 正確に言うと, 「スクリプトが意味する処理を実行するScriptRunner<T>
型delegate
を取得する」メソッドなんですが.
ScriptRunner<T>
型はここに定義されてまして, 要はGlobalObjectとCancellationToken
を渡してスクリプトの戻り値を受け取る非同期メソッドですね.
「何度も処理を実行し, 戻り値さえ取得できればいい」といった場合はこのメソッドでdelegateを取得すると便利です.
コード例を次に示します.
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;
}
フィボナッチ数列を出力するプログラムです.
要らないとは思いますが, 出力は次のような感じになります.
0
1
1
2
3
5
8
13
21
34
55
89
3.5 RunFromAsync
メソッド
ContinueWith
メソッドで連結したスクリプトを, 途中までの実行結果を引き継いで実行する際に使用します.
例として, 次のコードをご覧ください.
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]);
}
このコードを実行すると, 次のような出力を得ることができます.
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
型インスタンス**です.
言葉だけで説明されても…って方は, 次のコードを読むと理解できると思います.
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
なのには変わりなし) が入ってます.
以下にサンプルコードを示します.
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);
}
このコードの実行結果は次のとおりです.
~~~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)
が記録されています.
以下にサンプルコードを示します.
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"}");
上記コードの実行結果は次のとおりです.
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)
- 自動で生成された(
まぁ, 普通の使い方(?)をしていれば, スクリプト内で手動で定義したフィールドを取得できないなんてことはないと思います.
以下にサンプルコードを示します.
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}");
出力は次のとおりです.
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.IsReadOnly
がTrue
になる - 基本は宣言した順に入る. ただし,
static
やconst
キーワードを付けたフィールドについては末尾にstatic
→const
の順で入れられる
4.5 GetVariable
メソッド
Variable
プロパティで取得できるフィールド配列から, 特定のフィールド名を持つものを取得するメソッドです.
取得に失敗した場合はnull
が返ります.
サンプルコードは省略します. さすがにわかるよね…?
4.6 ContinueWithAsync
メソッド
「実行結果の取得」ではないですが, 同じScriptState
クラスに含まれているので, ここで紹介します.
このメソッドは, 現在の状態を保持して指定のスクリプトを実行し, 結果を取得するメソッドです.
インタラクティブな実行環境を提供したい場合に最適です.
例えば, Sample_6.csの「フィボナッチ数列を生成する処理」をGlobal Objectを使用せずに書いてみます.
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);
}
このコードを実行すると, 次のようにフィボナッチ数列を得ることができます.
0
1
1
2
3
5
8
13
21
34
55
89
スクリプト実行機能の各種オプションを弄る (ScriptOptions
クラス)
ここまでで, おそらく「スクリプトを読んで, 実行する実装」はできたのではないかと思います.
で. ここからは, 自身の目的に最適なオプションを選択するために, ScriptOptions
クラスおよびそこで使用する各種クラスを見ていきます.
ScriptOptions.Default
はCSharpScript.Create
メソッド等でoptions
にnull
を指定した際に使用されます.
ではでは.
パブリックメンバを列挙してみます.
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.Default
にWith~~~
メソッドやAdd~~~
メソッドをつらつら連ねていく感じになります.
さすがにメソッドに説明は要らないでしょうから, 各プロパティの意味だけ書いておきます.
1. MetadataReferences
プロパティ
スクリプト内でデフォルトで (#r
ディレクティブなしに) 使用できるアセンブリ群です.
デフォルトでは25個のアセンブリがリストされてます.
とはいえ, ここで指定されたstringはUnresolvedMetadataReference
クラスのコンストラクタに渡されて, その渡されたstringが記録されるだけなので, この時点でアセンブリが読み込まれているわけではありません.
With~~~
メソッドやらAdd~~~
メソッドやら, 引数の型の違いで色々ありますが…
-
string
(あるいはstring[]
系)に関してもUnresolvedMetadataReference
に変換されるだけで, 実際のロードはスクリプト実行時になります. -
Assembly
(あるいはAssembly[]
系)に関しては, いずれもCreateReferenceFromAssembly
メソッドからMetadataReference.CreateFromAssemblyInternal
メソッドを利用してMetadataReference
型に変換しています.- ちなみに,
CreateFromAssemblyInternal
メソッドを見ればわかるように,Assembly.Location
プロパティがnull
もしくはEmptyだと弾かれます.
- ちなみに,
2. MetadataResolver
プロパティ
MetadataReferenceResolver
クラスは, 簡単に言うと, 参照するDLLやEXEファイルを探すためのクラスです.
通常はデフォルトのままで十分だと思われます.
最終的にMicrosoft.CodeAnalysis.CSharp
namespaceのCSharpCompilationOptions
クラス… というよりかはその基底クラスのMicrosoft.CodeAnalysis
namespaceの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
クラスを見落としていました…
RuntimeMetadataReferenceResolver
は ScriptMetadataResolver
を介して触ることになります
詳細は末尾に追記します
なお, このクラスは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
: パスの先頭を置換する際のパターンを指定します.-
NormalizePath
メソッドで使われていて, 使用先のPathUtilities.NormalizePathPrefix
メソッドを見てみると, 配列をforeach
で回して, パスの先頭がKey
に一致したら一致部分をValue
に置換して返すような実装になっています.
-
#load
ディレクティブで相対パスを指定しても, デフォルトのままだとCurrentWorkingDirectoryからの相対パスとして解釈されてしまうため, baseDirectoryをスクリプトファイルが存在するディレクトリにセットしてあげる必要があります.
あるいは, ScriptOptions.FilePath
にロード元のスクリプトファイルのパスを指定しても大丈夫です.
4. Imports
プロパティ
C#10.0でいうglobalなusingディレクティブ
です.
対象のスクリプトファイルで, 指定したusing
が不要になります.
例えば, 次のようなコードを実行できます.
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
プロパティ
スクリプトがファイルから読み込まれている場合に, スクリプトファイルへのパスをセットします.
EmitDebugInformation
をtrue
にしたうえで, 必要に応じてFileEncoding
を設定し, このFilePath
プロパティにスクリプトファイルへのパスを指定することで, スクリプトのデバッグが可能になります.
ちなみに, string
でCreate
する場合はFileEncoding
の指定が必須になりますので, ご注意ください.
stringで渡す際のサンプルを次に示します.
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
プロパティ
整数演算のオーバーフローチェックを行うかどうかです.
以下に例を示します.
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倍にしているのでオーバーフローは確実なんですが, 出力を見ればわかるように, CheckOverflow
をfalse
にしておくとException
がthrowされません.
=== 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
であるRuntimeMetadataReferenceResolver
はinternal
なクラスなので, デフォルトでは指定されていないbaseDirectory
を指定したインスタンスを作って使う…ということは難しいです.
[2021年9月11日追記]
baseDirectory
等はScriptMetadataResolver
を介して触ることができました…
とはいえ, NuGetサポートはないので, それを求める場合は自力で実装することになります.
ということで, 独自のMetadataReferenceResolver
を作ってみましょう.
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
経由で資源を再利用しちゃいましょう.
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
ディレクティブをサポートしてなかったりしますが)
一応abstract
なNuGetPackageResolver
クラスが定義され, 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
クラスが使用されていました.
RuntimeMetadataReferenceResolver
はScriptMetadataResolver
から触ることになります.
ScriptMetadataResolver WithSearchPaths(params string[] searchPaths)
ScriptMetadataResolver WithSearchPaths(IEnumerable<string> searchPaths)
ScriptMetadataResolver WithSearchPaths(ImmutableArray<string> searchPaths)
ScriptMetadataResolver WithBaseDirectory(string? baseDirectory)
プロパティを変更する系のメソッドはこれだけです.
WithSerchPaths
メソッドはRelativePathResolver
クラスに渡すパス群を変更するメソッドです.
アセンブリを探すディレクトリの完全修飾パス(絶対パス)を指定します
WithBaseDirectory
メソッドは, SourceReferenceResolver
クラスと同じくアセンブリ検索の起点となるディレクトリです.