C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

C#: C# コードから ECMAScript ソースを翻訳&実行する処理系を組み込む方法; WebBrowser, JScript, V8 ( ClearScript )

いくつか手段がある。

  1. ECMAScriptMicrosoft 処理系の1つ ChakraCore を内包する WPFWebBrowser コンポーネントを組み込む方法
  2. ECMAScriptMicrosoft 処理系の1つ JScript を組み込む方法
  3. ECMAScriptGoogle 処理系の V8 を組み込む方法
  4. ECMAScript 処理系を内包する node.js を組み込む方法

など他にもいくらか。

今回は、 WebBrowser, JScript, V8 ( ClearScript ) について簡単な ECMAScript コードを評価する方法を試したので以下にメモを残します😃

1. WebBrowser を使う方法

  • どこかに見えない WebBrowser を配置しておいて裏でこっそり使うなり、コードだけで new して使うなり。
  • お好みの ECMAScript を記述した html を Navigate して InvokeScript †1 する。
    • html はメモリーストリームや string から読ませるのは少し面倒なのでリソースに持っておいて UriNavigate するのが楽。
  • WinForm 版と WPF 版は似たような設計ながら互換性が無いのでいんたーねっつの情報には注意。

C#ECMAScript を実行する方法をぐぐるとたいていこの類似手法が出てくる。この方法はコード例とか示すまでもないかなーと思うし、めんどくさいので実装例は省略。

2. JScript を組み込む方法

  • プロジェクトの References へ Microsoft.JScript Microsoft.CSharp を追加。
  • ↓な感じで使う。ちょっとめんどくさい。
using System.CodeDom.Compiler;
// 任意の ECMAScript を評価できそうなチート的なメソッドを翻訳しておいて
// ユーザーから任意の ECMAScript ソースを受け取って実行結果を取得できるつもりの JScript 実装
var r = 
  ( CodeDomProvider
    .CreateProvider( "JScript" )
    .CompileAssemblyFromSource
      ( new CompilerParameters() { GenerateInMemory = true }
      , @"package _{ class _{ function f( a ) { return eval( a ); } } }"
      )
    .CompiledAssembly
    .CreateInstance( "_._" )
    as dynamic
  ).f( " 1 + 2 + 3 - 4 " )
  // ↑ こういう数値計算とか文字列処理の式がユーザーから飛んでくる分には期待動作します。
  ;
MessageBox.Show( r.ToString() );

dynamic とかたまには使ってみたくなったので使った。 dynamic しない場合は記述量は増えるが、

// assembly を一端保持しておいて
var a = CodeDomPrivider. /* 中略 */ .CompiledAssembly();
// 型(=JScriptのソース定義における `class` )を取得して
var t = a.GetType( "_._" );
// 型のインスタンスを取得して
var i = Activator.CreateInstance( t );
// 型のメンバー・メソッドを実行
var r = t.InvokeMember( "f", System.Reflection.BindingFlags.InvokeMethod, null, i, new object[] { " 1 + 2 + 3 - 4 " } );
// 結果を表示
MessageBox.Show( r.ToString() );

のように実装する事になる。

この実装方法では、任意の ECMAScript コードをユーザーから受け取って実行できそうに視えたかもしれないが、できない。例えば、ユーザーに "function ( a ) { return a * a; }" のように関数オブジェクトのソースを与えさせて、 r.f( 10 ) のように実行して使おうとすると例外で死ぬ。

ユーザーに関数を定義させたい場合は、

// ユーザー入力の想定
var source = @"( a, b ) { return Math.sqrt( ( a * a ) + ( b * b ) ); }";

のような関数の引数と本体の定義を受けて、

// CompileAssemblyFromSource にはこんな具合で包んで関数 f という事にして翻訳させておいて
$"package _{{ class _{{ function f { source } }} }}"

それで使うとか、そういう事になる。

私は JScript には興味がかなり希薄なのでよく知りませんが、少なくとも現在のそれは、 .netソースコード記述言語の1つであり、 C#, VB.net, F# とおそらくほぼ等価な実装を ECMAScript 派生の言語処理系として実装したもの、あるいはそんなようなものなのでしょう。

そういうわけで、 Microsoft 環境での C# 的にはこの方法は ECMAScript らしきスクリプト言語処理系をアプリへ組み込む方法としては、ある意味ではもっとも楽なものの1つかもしれません。ある意味では。

昔から一部界隈にはたいへん忌み嫌われて来た JScript の独自仕様や新しい ECMAScript 標準への追従性、 ChakraCore が廃止されたら処理系はどうなるのか、何かと面倒事の原因を抱え込む危惧をひしひしと感じるので、私はあまり使いたくありませんけどね♥

要求が ECMAScript ではなく、明確に JScript な場合にはもちろんこの方法は最適と思います。また、この方法と同様にして C#, VB.net ソースのスクリプト処理系や、実行時に .net な DLL を翻訳してリンクするとかそういう荒業が必要な場合の参考にはなるかもしれません。

V8 を組み込む方法

  • プロジェクトの References へ NuGet から v8.redist-v141-x64 あるいは開発環境にあわせたそういうのを追加。
    • NuGet から "v8" で検索するとたくさんでてくる。 x86, x64, v140(=vs2015), v141(=vs2017), v120, symbols, redist, full などの組み合わせ。

で簡単に使えるかと思ったのだけど、 icui18n の参照が無いとかで NuGet からの install は失敗するようだった。

f:id:USAGI-WRP:20190116234427p:plain

redist ではなく full でも同様だったし、どこの誰がどうパッケージングした icu が欲しいのか探すのも面倒なので、今回はそもそも別系統の V8 の NuGet パッケージを試す事にした。

  • プロジェクトの References へ NuGet から Microsoft.ClearScript を追加。
    • Microsoft 製の V8 処理系を .net へ組み込むライブラリーらしい。( JScript 互換っぽい表記もあるのが懸念点じゃが…)

f:id:USAGI-WRP:20190116235031p:plain

  • ↓な感じで使う。
using Microsoft.ClearScript.V8;
// 単純な加減算の式を評価する例
var e = new V8ScriptEngine();
var r = e.Evaluate( " 1 + 2 + 3 - 4 " );
MessageBox.Show( r.ToString() );

こんだけ😃

// 関数を評価して dynamic で受けておいて C# 実装側でその関数を実行する例
var e = new V8ScriptEngine();
dynamic f = e.Evaluate( " ( a, b ) => Math.sqrt( a * a + b * b ) " );
var r = f( 3, 4 );
MessageBox.Show( r.ToString() );

わーい簡単だ😃 それにナウい感じの Arrow も使える。さすが愛され続けて頭おかしい高速化と進化を遂げるに至った V8 さんです。

欠点は V8 のアッセンブリー追加によるフットプリントの増加がちょこっと大きいくらいかな。 ClearScript の DLL は 445KB 。参考として JScript の DLL は .net Framework の一部な上、 129KB です。PC向けでは気にするシーンは滅多に無いかもしれません。安心安全で簡潔に使える V8 処理系の恩恵の方がよほど大きいと思います。

ちなみに、 "string".substr(-1) を評価した結果は "g"でした。 Stackoverflow によるとこの挙動は JScript の仕様では "string" になるらしいので試してみましたが、先の JScript でも "g" が得られてしまうのでバグとして修正されたか ECMAScript へ準拠するよう JScript の仕様が修正されたのかもしれません。 †2

また、 ClearScript は Microsoft 製らしさの恩恵と思える点に、 ConsoleWPFコンポーネントなどホスト環境の何かしらを束縛して扱う仕組みが実装されている点も便利が良いかもしれません。そこは私もまだ試していませんので機会があればメモを書こうと思います。†3

参考