JScript.eval にハァハァした。

C#でプログラム作っていて、テキストボックスに何らかの数式を入力し、それを評価する機能を付けたかった。
とりあえず言語はJavaScriptあたりがいいだろうと思って、ちょっと探した。

このページはいろいろな例を取り上げていて参考になる。
http://dobon.net/vb/dotnet/programing/eval.html

最初はScriptControlを使って実現しようとした。
問題なく出来た。
しかし、配布しようとすると、Interop.MSScriptControl.dll という、微妙な名前のDLLを付けなければならない。
しかもこのScriptControlがどのくらい普遍的にインストールされているものなのかというあたりもよくわからない。
のどに魚の骨が引っ掛かったような、惜しい感じ。

次に、内部で動的に編集したソースをコンパイルし、インスタンス化して呼び出す、というのを試してみた。
これもまあ出来た。
内部でコンパイルを呼び出したり、メモリ上に展開されたアセンブリを読み込んで、リフレクションで型名とメソッド名を指定してInvokeするなど、かなり勉強にはなる。
コンパイルしているのでスピードは問題ないし、詳細なコンパイルエラーも取得できる。

ちなみにこんな実装。

 JScriptCodeProvider codeProvider = new JScriptCodeProvider();
 CompilerParameters cp = new CompilerParameters();
 cp.GenerateInMemory = true;  //メモリに展開
 
 //JSコード
 String code = @"
class Evaluator {
 public function Eval(item, window) : boolean {
  return (" + statement + @");
 }
}";
 
 //コンパイル
 CompilerResults cr = codeProvider.CompileAssemblyFromSource(cp, code);
 if (cr.Errors.HasErrors)
 {
  //メッセージボックスを表示する、みたいな感じ
 }
 
 //インスタンス
 this.instance = cr.CompiledAssembly.CreateInstance("Evaluator");
 MethodInfo evaluator = cr.CompiledAssembly.GetType("Evaluator").GetMethod("Eval");
 
 //メソッド呼び出し
 Object ret = evaluator.Invoke(this.instance, new Object[] { item, window });

なんか汚くてすいません。
これで statement に 1 == 1 とか与えるとちゃんと True が返ってくる。
でも、functionは定義できなかった。
JScript.NETの仕様なんだろうか。
codeの部分をうまく仕込めば回避できる?

ちなみに item や window は自前のオブジェクトで、item.name や window.status などのアクセサを定義してある。
(window.status を変更すると、Labelコントロールのテキストが変わる。)
プロパティの取得も出来るし、UIのテキストも変更できたよ。
JavaScript は引数の型を気にせずに受け取り、何も考えずに name や status をインボークしてくれる。
動的型付けのそこらへんが羨ましい。C#使いとしては。

とまあ、これで目的は果たせるのだが、新たな問題が判明。
動的にコンパイルしてメモリに展開したアセンブリは、使い終わったらそこだけアンロードすることができないのだという。
呼び出し側が全終了するまでずっとメモリに残っちゃうらしいのだ。
回避する方法もあるようだ(別アプリケーションドメインに読み込む)が、これ以上難しくなると私にゃ保守できない。
うーん。やっぱり ScriptControl かな・・・

と思っていたが、今日になって JScript の eval というメソッドの存在を知った。
上であげた参考サイトにも書いてある(しかも一番上)んだけど、Vsaという名前空間が2.0で非推奨になってるらしいので無視してた。
しかし MSDNで eval の解説を見ると、
http://msdn2.microsoft.com/ja-jp/library/b51a45x6(VS.80).aspx
このサンプルソースはよーく見るとけっこうエグい。
eval の中と外って同じレベルなのね。

じゃあ、JScriptで式評価用のクラスを作って、C#からそれを召喚すればいいじゃん?

VisualStudioはJScriptコンパイルしてくれないので、コマンドプロンプトから手動で以下のソースをコンパイルした。
JsEval.js

package JsEvaluator {
 public class JsEval {
  function Eval(statement : String, item : Object, window : Object) : Boolean {
   return eval(statement, "unsafe");
  }
 }
}

コンパイル

jsc /t:library /out:JsEval.dll JsEval.js

でもって、C#からはこう呼び出す。

  JsEval eval = new JsEval();
  Object ret = eval.Eval(statement, item, window);

うわー。めっちゃシンプル。
"unsafe"というのがきな臭いけど、これをつけることでfunctionも使える。
itemやwindowへのアクセスも全く問題なし。
構文におかしなところがあれば、例外を投げてくるので適切にキャッチすればよい。
これぞ俺の求めていたものやーー
※外部から与えたObjectにJScriptでプロパティやメソッドを加えることは出来ない。
 設定してもエラーにはならなかったが、取得は出来なかった。
 eval内で新たに定義したvar変数は、JavaScriptらしく、自由にプロパティ・メソッドを追加できる。

JsEval.dllを同梱しなきゃならないのはScriptControlのときと同じだけど、.NETの標準機能を使っているという安心感はある。
それに、インターフェースも継承もなしに、Objectでぽいと渡して、スクリプトでガチャガチャアクセスできるってのはなんか楽しそうだ。
これを使えば、
「普段まじめなC#が、あんなにもダイナミックになるなんて…。ハァハァ
 JScriptってなんてHなんだ!」(※)
な妄想が現実のものになるかもしれない。

※H…「日陰者」「日の当たらない」の意味。
 JScriptも実行ファイルやDLLを作れるのに、使ってる話をあまり聞かない。
 まあ、C#もかなり使いやすくなって、コンパイルしてまでJScriptにこだわる理由がなくなってるんだろうな。