Bakulog

獏の夢日記的な何か。

【C#】C#スクリプトが外部リソース参照するときのパス設定【スクリプト】

タイトル通り、C#スクリプトから他ファイル(スクリプト/アセンブリ)を読み込ませるための設定について簡単に。

 

本記事の内容は以前に書いた下記の2記事と関係があります(内容も若干被ってます)。「C#スクリプトってどうやってプログラムから実行するの?」という所から始めたい方は上側の「【C#】Roslyn for Scripting試してみた【スクリプト】」をご覧ください。

 

概要と目次

Roslyn for ScriptingでC#スクリプトを実行する方法については早い段階からkekyoさんの「Roslyn for Scriptingで、あなたのアプリケーションにもC#スクリプトを!!」という記事で基本事項があらかた紹介されているのですが、今回はコチラの記事に載っていない#loadや#rディレクティブといった外部スクリプト/リソースの読み込みについて紹介していきます。

 

もくじ

 

 

はじめに. 状況設定

ここの手順は【C#】Roslyn for Scripting試してみた【スクリプト】で紹介してるのとほぼ同じです。

  1. コンソールアプリケーションを作成
  2. プロジェクトのプロパティから.NETのターゲットを4.6以上に設定
  3. NuGetパッケージマネージャでMicrosoft.CodeAnalysis.CSharp.Scriptingをインストール
  4. 本体のプログラムを次のようにする
using System;
using System.IO;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.CodeAnalysis.CSharp.Scripting;

namespace HelloRoslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            var so = ScriptOptions.Default;

            var script = CSharpScript.Create(File.ReadAllText("myscript1.csx"), so);
            script.RunAsync().Wait();
        }
    }
}

 

最後にテキストとしてスクリプトファイルを2つ用意します。スクリプトはいずれも実行ディレクトリに配置してください。

myscript1.csx

#load "myscript2.csx"

DoSomething();

myscript2.csx

using System;

void DoSomething() => Console.WriteLine("This is sub method");

"myscript1.csx"の方に載っていますが、このように#load "(スクリプト名)"と書くことで他のスクリプトをロードすることが出来ます。

 

なお実行ディレクトリにスクリプトを置く方法としては直置きでも構いませんが、下記のような手順を取る方がオススメです。

  • C#ソース(クラスファイルとかのプレーンなもの)として"myscript1.csx"とか"myscript2.csx"をVisual Studioで追加。拡張子は必ず".csx"にする
  • ソリューションエクスプローラスクリプトファイルを選んでプロパティを開き、「出力ディレクトリにコピー」の設定を「常にコピー」または「新しい場合はコピー」に変更

拡張子が".csx"になっているとVisual Studio側もある程度インテリセンスを利かせてくれます。

 

 

その1. スクリプトのロード時パス解決

上記のプログラムでは、デフォルトの設定で2つのC#スクリプトが連携するように書けているように思えます。なんとなく動きそうなので実行してみますが…

roslyn_script_load_error

はいダメですね。エラーメッセージによるとスクリプト"myscript2.csx"が見つからないと言っています。「カレントディレクトリすら調べないのかお前は」とツッコミの一つも入れたくなりますがグッとこらえ、Mainメソッドを以下のように書き換えます。

static void Main(string[] args)
{
    var ssr = ScriptSourceResolver.Default
        .WithBaseDirectory(Environment.CurrentDirectory);
    var so = ScriptOptions.Default
        .WithSourceResolver(ssr);


    var script = CSharpScript.Create(File.ReadAllText("myscript1.csx"), so);
    script.RunAsync().Wait();
}

ScriptSourceResolverというやつが現れました。コイツを使うことで、スクリプトをロードする際に参照するパス(やパスの候補)が設定できます。今回はBaseDirectoryという基底になるパスの設定をカレントディレクトリに変更しており、コレで#loadディレクティブで指定したスクリプトはカレントディレクトリとの相対パスとして探索されるようになります。

今回は紹介しませんが、より細かい設定を行う場合はWithSearchPathというのを使います。コチラではPythonで言うところのsys.pathみたいに複数のスクリプト検索パス候補を指定できます。

 

なお今回の例に限った事ではないのですが、ScriptOptionsをはじめスクリプトの実行設定に関係あるクラスでは基本的に、staticプロパティとして用意されたデフォルト設定(Default)を最初に取得してから"With~~"というメソッドを呼び出して設定を追加していくようなスタイルが使われます。Visual Studioのインテリセンスを活用する際の参考にしてください。

 

ともかく、これで実行すると今度はきちんと動きます。

roslyn_load_script_success

 

 

その2. スクリプトではなくコードでアセンブリを登録する

こちらはスクリプトのロードより実用性が高そうな内容です。C#スクリプトは外部の何かと連携して使うためのモノですから、例えば外部DLLを読み込んでpublicなAPIを叩く、といった状況が考えられます。DLLを読み込む簡単な方法としては下記の2通りが考えられます。

  • ScriptOptionsに変更を加えて最初から参照を追加しておく
  • #r ディレクティブを使ってスクリプトの実行時にアセンブリを読み込む

まずはラクちんな方法である「ScriptOptionsに変更を加えて最初から参照を追加しておく」をやってみます。本体側のコードを次のように書き換えます。

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

namespace HelloRoslyn
{
    class Program
    {
        static void Main(string[] args)
        {
            var ssr = ScriptSourceResolver.Default
                .WithBaseDirectory(Environment.CurrentDirectory);
            var so = ScriptOptions.Default
                .WithSourceResolver(ssr)
                .WithReferences(Assembly.GetEntryAssembly());

            var script = CSharpScript.Create(File.ReadAllText("myscript1.csx"), so);
            script.RunAsync().Wait();
        }
    }

    public class MyClass
    {
        public void MyMethod()
        {
            Console.WriteLine("this is MyMethod in MyClass");
        }
    }
}

下のほうではアセンブリとして公開するクラス"MyClass"を追加してますが、ポイントはそこではなくScriptOptionsの設定で追加してるWithReferenceメソッドの呼び出し部分です。メソッドの引数に参照させたいアセンブリを直接登録するだけなので簡単ですね。今回はシンプルなケースとして本体側のexeを登録しています。

 

これでスクリプト"myscript1.csx"を次のように書くとMyClassをロードして関数が実行できます。using文で指定する名前空間名は当然本体側のプログラムによって変わるのでそこだけ気を付けてください。

using HelloRoslyn;

var mc = new MyClass();
mc.MyMethod();

 

 

その3. そうじゃなくて私は動的にDLLをロードしたいんですよ

サブタイトルの通りですが、上の方法ではコンパイル時に参照先のアセンブリが決まってなければいけません。これではスクリプトのダイナミックな実行という意味で少し物足りないので別のことをやってみます。

あくまで一例になりますが、例えばC#スクリプトJSONを必要とするAPIへの接続に使われるようなシナリオを考えると、Newtonsoft.JsonC#スクリプト側で動的にロードしたくなるかもしれません。そのような想定でちょっと動作を試してみましょう。

NuGetパッケージとしてNewtonsoft.Jsonをインストールし、スクリプト"myscript1.csx"を次のように書き換えます。

#r "Newtonsoft.Json.dll"

using System;
using Newtonsoft.Json.Linq;

var jstr = new JValue("This is Newtonsoft.Json test");

Console.WriteLine(jstr);

読み込みたいアセンブリ#r "(アセンブリ名)"の形で指定して使っているほかは普通のC#コードです(JValueはNewtonsoft.Json.Linqで定義されている割と普通な型です)。さあ実行しましょう!

roslyn_r_directive_error

はい、例によってダメです。スクリプトをロードする時とほぼ同じ「そのアセンブリ見つかりません」的なエラーメッセージが表示されています。

 

余談: 一つ注意ですがスクリプトのロードと異なり、アセンブリのロードに関してはアセンブリがGAC(Global Assembly Cache)に登録されているかどうかで挙動が変わります。GACに登録されてるSystem.Windows.Formsなどのアセンブリであれば特に設定が無くても#rディレクティブで読み込んで利用できます。GACのアセンブリを使う例については【C#】Roslyn for Scripting C#小ネタ集【Scripting】でいくつか紹介しています。

 

ともかくエラーをどうにかしましょう。エラーの内容はスクリプトのロード時とほぼ同じなので対処法も非常に似通っています。本体側のMainメソッドを次のように書き換えます。

static void Main(string[] args)
{
    var ssr = ScriptSourceResolver.Default
        .WithBaseDirectory(Environment.CurrentDirectory);
    var smr = ScriptMetadataResolver.Default
        .WithBaseDirectory(Environment.CurrentDirectory);
    var so = ScriptOptions.Default
        .WithSourceResolver(ssr)
        .WithMetadataResolver(smr);

    var script = CSharpScript.Create(File.ReadAllText("myscript1.csx"), so);
    script.RunAsync().Wait();
}

ScriptSourceResolverとほぼおなじ見た目になってますが、ScriptMetadataResolverを新しく設定に使っています。こちらではスクリプトではなくアセンブリを拾う際の設定がで、今回の場合はこちらでもカレントディレクトリをベースに探索するように変更しました。

これで今度は正しく動作します。

 

 

その4. WithFilePathという別解

実はスクリプトのロードやアセンブリの参照解決は次のように書くことでもなんとかなります。

var so = ScriptOptions.Default
    .WithFilePath(Path.GetFullPath("myscript1.csx"));

こちらは「読み込んだスクリプトの位置」を明示的に指定することで相対パスによる処理を可能にしています。個人的にはBaseDirectoryを差し替える手順の方が安心感があって好きなのですが、状況によって使い分けて下さい。

 

まとめ

前の記事と被ってるところもあったのですが、スクリプトのロード(#load)とアセンブリのロード(#r)をまとめて紹介した記事があった方がいいかなあと思って別途書いてみました。いかがだったでしょうか。

ウワサによるとNuGetパッケージを実行時に用意することも出来るらしいのですが、そちらについては未確認です。確認したうえで日本語情報が無かったら何か書くかもしれませんので続報にご期待下さい。