Edited at

【C#】Roslyn for Scriptingについて

More than 1 year has passed since last update.


はじめに

Roslyn for Scriptingについていろいろ触ってみたのでまとめました。


動作環境

Visual Studio 2017 Community

.NET Framework 4.6.2

Microsoft.CodeAnalysis.CSharp.Scripting 2.1.0


使い方

nugetでパッケージをインストール出来ます。

Install-Package Microsoft.CodeAnalysis.CSharp.Scripting

パッケージインストール後、Roslyn for ScriptingのAPIが使用できるようになります。

以下、サンプルコードを示します。

using System;

using System.IO;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace Sample
{
class Program
{
static void Main( string[] args )
{
try {
var script = CSharpScript.Create( File.ReadAllText( "hello.csx" ) ) ;
script.RunAsync();
}
catch( CompilationErrorException ex ) {
Console.WriteLine( "[Compile Error]" );
Console.WriteLine( ex.Message );
}
catch( Exception ex ) {
Console.WriteLine( ex );
}
}
}
}

//hello.csx

using System;

Console.WriteLine( "Hello Roslyn for Scripting!" );

その他のサンプルコードについてはRoslynのgithubのwikiにあります。

https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples

また、日本語でのサンプルならば以下のサイトが一番まとまっていると思います。

http://wiki.clockahead.com/index.php?Coding/.NET/Roslyn/Scripting


トラブルシューティング

今までハマったことについて。同じ現象が起きた方の助けになれば幸いです。


nugetでのインストールに失敗する(エラーが出る)

ターゲットフレームワークが.NET Framework 4.6(.NET Core 1.1)以前のバージョンになっている可能性があります。プロジェクトのプロパティからターゲットフレームワークを変更出来ます。


初回だけスクリプト実行まで時間が掛かる

初回だけRunAsync呼び出しからスクリプト実行まで時間が掛かります。

一応issueとして挙がっているようですが、現在(V2.1.0)はまだ修正されていないようです。

https://github.com/dotnet/roslyn/issues/16897


非同期のはずなのに同期処理になる

RunAsync()は非同期メソッドでawaitしていても同期になり、GUIの場合はUIが固まる事があります。

スクリプトで同期処理を行っている事が原因です。非同期処理にすると非同期になります。

//同期の場合

using System;
using System.Threading;

Thread.Sleep( 1000 );
Console.WriteLine( "1ms elapsed" );

//非同期の場合

using System;
using System.Threading.Tasks;

await Task.Delay( 1000 );
await Task.Run( () => Console.WriteLine( "1ms elapsed" ) );


RunAsyncのキャンセルが出来ない

RunAsyncには他のTaskと同様にcancelationTokenを渡すオーバーロードが存在しますが、現在スクリプト実行中にキャンセルさせる事が出来ません(スクリプト実行前にキャンセルした場合はOperationCanceledExceptionが発生してキャンセルされます)。

以下の関数を呼び出し、関数内のCancelメソッド呼び出し後もキャンセルされずにスクリプトを実行し続けます。

コードを読んでみると内部でThrowIfCancellationRequestedメソッドを呼び出しているようにも見えるのですが・・

public async static Task RunTaskAsync()

{
try {
var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ) );
var cancelTokenSrc = new CancellationTokenSource();

var cancelTask = Task.Run( () =>
{
Thread.Sleep( 4000 ); //スクリプト実行まで時間が掛かるので適当にwait
cancelTokenSrc.Cancel();
Console.WriteLine( "Cancel executed" );
} );

await Task.WhenAll( script.RunAsync( cancellationToken: cancelTokenSrc.Token ), cancelTask );
}
catch( CompilationErrorException ex ) {
Console.WriteLine( "[Compile Error]" );
Console.WriteLine( ex.Message );
}
catch( TaskCanceledException ) {
Console.WriteLine( "Task is canceled" );
}
catch( OperationCanceledException ) {
Console.WriteLine( "Operation is canceled" );
}
catch( Exception ex ) {
Console.WriteLine( ex );
}
}

//delay.csx

using System;
using System.Threading.Tasks;

for( int i = 0; i < 10; i++ ){
await Task.Delay( 1000 );
await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );
}


実行結果

1000ms elapsed

2000ms elapsed
Cancel executed
3000ms elapsed
4000ms elapsed
5000ms elapsed
6000ms elapsed
7000ms elapsed
8000ms elapsed
9000ms elapsed
10000ms elapsed

対策として、スクリプト側でキャンセルするかスクリプト内で呼び出すメソッド内でキャンセルする事でTaskCanceledExceptionをthrowさせる事が出来ます。


スクリプト内でキャンセルする場合

public async static Task RunTaskAsync()

{
try {
var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ) );
await script.RunAsync();
}
catch( CompilationErrorException ex ) {
Console.WriteLine( "[Compile Error]" );
Console.WriteLine( ex.Message );
}
catch( TaskCanceledException ) {
Console.WriteLine( "Task is canceled" );
}
catch( OperationCanceledException ) {
Console.WriteLine( "Operation is canceled" );
}
catch( Exception ex ) {
Console.WriteLine( ex );
}
}

//delay.csx

using System;
using System.Threading;
using System.Threading.Tasks;

var cancelTokenSrc = new CancellationTokenSource();

for( int i = 0; i < 10; i++ ){
await Task.Delay( 1000, cancelTokenSrc.Token );
await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );

if( i == 3 ){
cancelTokenSrc.Cancel();
Console.WriteLine( "Cancel executed" );
}
}


実行結果

1000ms elapsed

2000ms elapsed
3000ms elapsed
4000ms elapsed
Cancel executed
Task is canceled


スクリプト内で呼び出すメソッド内でキャンセルする場合

public async static Task RunTaskAsync()

{
try {
var script = CSharpScript.Create( File.ReadAllText( "delay.csx" ), globalsType: typeof( CancelableAPI ) );
var api = new CancelableAPI();

var cancelTask = Task.Run( () =>
{
Thread.Sleep( 4000 ); //スクリプト実行まで時間が掛かるので適当にwait
api.Cancel();
Console.WriteLine( "Cancel executed" );
} );

await Task.WhenAll( script.RunAsync( api ), cancelTask );
}
catch( CompilationErrorException ex ) {
Console.WriteLine( "[Compile Error]" );
Console.WriteLine( ex.Message );
}
catch( TaskCanceledException ) {
Console.WriteLine( "Task is canceled" );
}
catch( OperationCanceledException ) {
Console.WriteLine( "Operation is canceled" );
}
catch( Exception ex ) {
Console.WriteLine( ex );
}
}

public class CancelableAPI

{
private CancellationTokenSource cancelTokenSrc;

public CancelableAPI()
{
cancelTokenSrc = new CancellationTokenSource();
}

public async Task Delay( int milliseconds )
{
await Task.Delay( milliseconds, cancelTokenSrc.Token );
}

public void Cancel()
{
cancelTokenSrc?.Cancel();
}
}

//delay.csx

using System;
using System.Threading.Tasks;

for( int i = 0; i < 10; i++ ){
await Delay( 1000 );
await Task.Run( () => Console.WriteLine( $"{( i + 1 )*1000}ms elapsed" ) );
}


実行結果

1000ms elapsed

Cancel executed
Task is canceled


おわりに

Roslyn for Scriptingについて記載しました。

Visual Basicの方についても現在開発中みたいですので今後、Visual Basic版のRoslyn for Scriptingも使えるようになるかもしれません。