式評価の実装を指定する語彙

式評価時の例外スローが性能に大きな影響を与えることが判ったため、式評価時にできるだけ例外をスローしないようにしたい。しかし、元々どのように書かれているか判らない評価式をパースしてエラーを返すのはスクリプトエンジン側の処理であり、コンテナ側ではやりたくない。かといって、設定された式(と思われる文字列)を無差別に評価して意味の無いエラーや例外をスローするオーバヘッドも避けたいのだが、どうしたらよいだろう。
現状で思いつくのは、式評価して欲しいケースを明示的に指定できるような語彙を使ったらどうだろう、ということだ。語彙が増えるのは嫌だが、例外が増えるのはもっと嫌なのでもっと良い方法を思いつくまではこの方法で行こう。

例えばPowerShellによる式評価を明示的に指定する場合はというタグ(Javascriptならばとか)で、評価する式を囲むようにする。コンテナ側では設定ファイルからIExpressionインタフェースの生成時にこのタグにより、適切な式評価のためのインスタンスを生成してやれば良い。

  • 例) 明示的に式評価を行う
<property name="Date"><ps>[DateTime]::now</ps></property>
<property name="Size"><ps>new-object System.Drawing.Size(100, 100)</ps></property>

明示的な親タグが無い場合は、ヌル実装で評価されるようにしておけば、式評価処理はすっ飛ばされて例外も減るはず。逆に明示的に式評価を指定した場合でエラーが発生したら、盛大に例外をぶち上げよう。

IExpressionインタフェースのヌル実装

評価式のインジェクションのテスト及び、各種実装(Javascript, PowerShell)の性能比較を行うためにIExpressionインタフェースのヌル実装を用意してみた。

public class NullExpression : AbstractDotnetExpression {
    public NullExpression(string expression)
        : base(expression)
    {}

    protected override object DoEvaluate(IDIContainer container)
    {
        return this.expression;
    }
}

見ての通り、ヌル実装なので内部の評価式をそのまま返すだけである。当然だが、このヌル実装が性能でもダントツに速い。アプリケーションの起動時間だけでも、Javascript実装に比べて1.5倍、PowerShell実装に至っては4倍速い。ちなみにPowerShell実装が遅いのは実装そのものよりもスローしている例外に原因がある。RunspaceInvoke#Invokeメソッドは式評価に失敗すると何故か、以下のような大量の例外をスローして大幅に性能を劣化させる。

'System.IO.DirectoryNotFoundException' の初回例外が mscorlib.dll で発生しました。
'System.IO.DirectoryNotFoundException' の初回例外が mscorlib.dll で発生しました。

  :
  :
  以下、32回に渡って繰り返し

'System.Management.Automation.CommandNotFoundException' の初回例外が System.Management.Automation.dll で発生しました。
'System.Management.Automation.CommandNotFoundException' の初回例外が System.Management.Automation.dll で発生しました。
'System.Management.Automation.CommandNotFoundException' の初回例外が System.Management.Automation.dll で発生しました。
'System.Management.Automation.PipelineStoppedException' の初回例外が System.Management.Automation.dll で発生しました。
'System.Management.Automation.CommandNotFoundException' の初回例外が System.Management.Automation.dll で発生しました。
'System.Management.Automation.CommandNotFoundException' の初回例外が System.Management.Automation.dll で発生しました。

再起しているのだろうか。この辺がまだRC(Release Candidate)所以。
実際問題、.NETの場合は型コンバータが充実しているので、式評価を諦めて、型変換だけを行うのであればヌル実装でも十分に実用になることが判っている。コンテナが常駐し続けるASP.NETの場合はPowerScript、スマートクライアントの場合はヌル実装という様に、用途に合わせて使い分けるのが良いのかもしれない。

RunspaceInvoke#Invokeの戻り値

RunspaceInvokeクラスのInvokeメソッドによるスクリプト式の実行結果の型Collectionだが、戻る値には別に深い意味は無かった。単に式評価の結果が複数あった場合に対応しているだけである。
例えば単純な例として、以下のようにC#からPowerShellスクリプトを実行した場合、

RunspaceInvoke invoker = new RunspaceInvoke();
string script = "get-process";
object result = invoker.Invoke(script);
foreach (PSObject obj in (Collection)result)
{
    Console.WriteLine(obj.BaseObject);
}

コンソールには、現在動いている全てのプロセスを列挙するSystem.Diagnostics.Processクラスのインスタンスの文字列表現が、タスクマネジャの「プロセス」で見るのと同じ数だけ出力されるはずである。ならば、わざわざ戻り値がPSObjectにラップされているのは何故か、というのが今後調べていく所だが、現在のところ恐らくこれはPowerShellのパイプラインに関係しているのではないかと思っている。
逆に言うと、戻り値が複数あった場合にDIコンテナの評価式としてはどう扱うかを考えなくてはならないということになる。

PowerShellにはまる

これまでの流れで週末からWindows PowerShellについて、いろいろと調べているのだが、これは非常に面白い。例えばCmdLet(コマンドレット)という機能があって、以下のようなC#で書いたクラス(勿論VBでも良いが)から自分だけのコマンドが作れるのだ。

using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation;

[Cmdlet("say","hello")]
public class CmdLetTest : Cmdlet {
    private string name;
    
    [Parameter(Mandatory=true, Position=0)]
    public string Name {
        get { return this.name; }
        set { this.name = value; }
    }
    protected override void ProcessRecord() {
        WriteObject("hello " + this.name);
    }
}

これをコンパイルしてできたアセンブリをPowerShellに登録すると、こんな風にコマンドとして起動できる。

PS say-hello 'Kazzz'
hello Kazzz 

ここでもカスタム属性が大活躍している。

  • [Cmdlet("say","hello")]

コマンドレットを定義する。パラメタは動詞+名詞で指定する。つまりこの場合、このコマンドを呼び出す際に使用する文法は"say-hello"ということになる。

  • [Parameter(Mandatory=true, Position=0)]

コマンドのパラメタを定義するMandatory=trueは必須であることを示し、Position=0とはパラメタの並びとして最初に指定するパラメタであることを示している。

コマンドで行う実際の処理はProcessRecord()メソッドをオーバライドしてその中に記述するのだが、ここでは何も値を戻しておらず、代わりにWriteObjectを使用しているのがミソ。先日のエントリで書いていた謎があったが

目下の問題は、Invokeメソッドで戻ったPSObjectクラスのコレクションに、一体どのような結果が返ってくる可能性があるか、現時点では調べていないために全く解らないことだ。現状で判明しているのはスクリプト式の実行が成功した際には、コレクションの序数0の要素に実行結果の戻り値がセットされている、という事実だけである。なさけない話だ。

この謎を解く鍵はここいらにあるのではないかと、現在も調査中。