ChakraCore implement
こっそり出したことで有名なMiscrosoft社製JavaScriptエンジンがChakraCoreです。格好よすぎる…。
DLLが出力できるので、C#じゃないDelphiで読み込めるか試してみたので一通り残しておきます。他言語でもヘッダがあれば大体同じなはずです。
- メモリ上読み込みと実行(文字列データから実行)
- 関数の登録、JS関数の呼び出し
- 値の設定と取得
- ユーザー定義クラスの受け渡しと解放
- array,object操作
- エラーハンドリング
あたりが出来ればとりあえずなんとか出来そうですね。(エラーハンドリングは長いので次に記事に分けました)
github
完成ソースコードを置いておきます。ChakraCoreとそのライブラリヘッダ抜かれています。導入については後記。
https://github.com/Ko-Ta2142/ChakraCore_DelphiSample01
ChakraCoreのコンパイル
・ChakraCore
https://github.com/Microsoft/ChakraCore
コンパイルはお任せ致します。
今回は32bitアプリケーションで動作を行うので「x86-release」で作ってみてください。
DLLヘッダファイル
こちらの方のを使用させて貰いましょう。
・ChakraCore Hello-world Sample
https://github.com/hsreina/Chakra-Samples-Delphi
ChakraCommon.h を整形したもので、命令名やパラメータなどは元ソースとほぼ同一です。C#などのサンプルともほぼ同じです。
使用には ツール - オプション - ライブラリ - ライブラリパス にこのライブラリヘッダのパスを追加するだけです。
ちょっとバグがあるので、あとで修正箇所も説明しますね。
DLLとのやりとりは stdcall になります。これは大本(c++)の CHAKRA_API にてexportと一緒に定義されています。
文字コード
ChakraCoreの文字列はwindowsではutf16でのやりとりが一般的です。
なのでDelphiであればstringのままでいいですし、c++ならstd::wstringで受け渡しすることになります。
幸いutfは変換コストが軽いので、utf8との連携でも大したことないでしょう。
まずは最小構成
中にあるサンプルがほぼオフィシャルのサンプルです。
・Delphi
https://github.com/hsreina/Chakra-Samples-Delphi/blob/develop/ChakraCore%20Samples/Hello%20World/Windows/Delphi/HelloWorld.dpr
・C++
https://github.com/Microsoft/Chakra-Samples/blob/master/ChakraCore%20Samples/Hello%20World/Windows/C%2B%2B/HelloWorld/HelloWorld.cpp
さらに実行のみ切り出せば以下になります。
JsCreateRuntime(JsRuntimeAttributeNone, nil, runtime); JsCreateContext(runtime, context); JsSetCurrentContext(context); JsRunScript(pwchar_t(script), @currentSourceContext, pwchar_t(''), result); JsSetCurrentContext(JS_INVALID_REFERENCE); JsDisposeRuntime(runtime);
JsRunScriptでutf16のpwcharバッファからスクリプトを読み込んでいますので、この段階でメモリ読み込みになっててありがたい。
引数の3番目はデバッグ用のファイル特定用のファイル名などを設定します。分かれば良いので何でも良いです。
最後の result は実行後の返値。最初にルート実行して構築するのはluaなどと同じようです。
サンプルの後半はこの return の値を取得して表示させるコードになっています。
script := '(()=>{return "Hello world!";})()';
即時実行と言うんでしたっけ。括弧が多いですが、無名関数がこんにちわ!をreturnするだけです。
function main(){ return "Hello world!"; } main(); //run
アプリに投げるのってreturn main(); じゃないんですね。関数実行を考えればそうかな?そうかもって感じです。
要素(property)操作
Chakraにおいてもluaよろしく、変数データはオブジェクト(テーブル)で構成されているので、まず必要になるのがそれらの要素 property の操作命令です。
一貫して値の操作はオブジェクトの何々〜という操作でやりとりを行います。
get [対象オブジェクト].[要素ID] -> [値] set [対象オブジェクト].[要素ID] <- [値]
みたいな感じですね。
この操作がこれから多くなるので、関数化しておきましょう。
要素IDは JsGetPropertyIdFromName で要素名を設定すれば作ってくれるの楽ちんです。
get とついてますが JsValueRef を生成するのでmakeとかcreateみたいな命令です。既存のものを指定しても新しく作られて置き換えられます。
function SetProperty(obj:JsValueRef; const name:string; value:JsValueRef):boolean; var idref : JsPropertyIdRef; begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsSetProperty(obj,idref,value,true) = JsNoError then exit; result := false; end; function GetProperty(obj:JsValueRef; const name:string; _type:JsValueType):JsValueRef; var idref : JsPropertyIdRef; t : JsValueType; begin if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if JsGetProperty(obj,idref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if _type = t then exit; result := nil; end; function DeleteProperty(obj:JsValueRef; const name:string):boolean; var idref : JsPropertyIdRef; ret : JsValueRef; // Whether the property was deleted. begin result := true; if JsGetPropertyIdFromName(pwchar_t(name),idref) = JsNoError then if jsDeleteProperty(obj,idref,true,ret) = JsNoError then exit; result := false; end;
取得の際は厳格性がほしいので型チェックを入れてあります。
ごめん、動かないわ
ソースは問題ないのですが、ここで、ヘッダに少々修正が必要です。
具体的には JsGetValueType を使用すると指定した JsValueRef(const) が変な数値になります。利かねぇconstだな!
原因は列挙型がChakraCoreは4byte(32bit)、Delphi側が初期では最小1byte(8bit)なのが問題です。
解決方法は2つで、プロジェクト設定で列挙型の最小値を4byteにするか、ChakraCommon.pasの先頭にコンパイルオプションを入れます。
unit ChakraCommon; //enum size 32bit {$Z4} interface
この方法はソースコード単位で利くので、こっちをおすすめします。
値の取得と設定
まずはグローバルに変数を作ったり、設定したり、取得したり、削除したりしましょう。
グローバルといっても、操作対象はグローバルという名前のオブジェクトです。このあたりもluaと同じです。
global : JsValueRef; ref : JsValueRef; a : integer; ... JsGetGlobalObject(global); // set / new JsIntToNumber(123456,ref); // double値の生成 SetProperty(global,'g_int',ref); // get ref := GetProperty(global,'g_int',JsNumber); JsNumberToInt(ref,a); Writeln('a is ' + IntToStr(a)); //print // delete DeleteProperty(global,'g_int');
グローバルオブジェクトは JsGetGlobalObject() だけで取得出来ます。あれ?これでは複数仮想機械を作った場合は…。
たぶん JsSetCurrentContext(context); このあたりで切り替えられると思っては居るのですが、まだ試してません。
任意のオブジェクトに対して操作する場合は、global を他のテーブルにすり替えるだけです。
ごめん、また動きません
豪快にクラッシュすると思います。ごめんね。
JsIntToNumber の宣言に問題があるので、ChakraCommon.pas の JsIntToNumber の箇所を検索で見つけて以下に修正してください。
function JsIntToNumber( intValue : int; //var intValue : int; //** fix var value: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
var抜くだけです。
さて、これで IntToNumber がちゃんと ValueRef かえすか確認してみると。
ref : 0x0000001F
というアドレスじゃ無い変な値が返ってきます。
あわわクラッシュじゃ!と思ったらちゃんと動きます…。不安。
大本のソースコードを見てみると
//If number is not heap allocated then we don't need to record/track the creation for time-travel if (Js::JavascriptNumber::TryToVarFast(intValue, asValue)) { return JsNoError; }
最適化してるみたいですね。
ししし知ってるわよ!タイムをトラベル、タイムトラベラーでしょ!
文字列
文字列も数値と同じです。
が、やりとりはutf16文字列の生ポインタを返すので、delphiだとstring、c++だとstd::wstringで扱うにはちょいと領域確保+コピーが必要です。
対象JsValueRefの生存期間も考えると、整数や浮動小数と同じくNativeで値を保持しておくのが安全です。
procedure MakeString(var value:JsValueRef; const s:string); begin if JsPointerToString(pwchar_t(s),length(s),value) = JsNoError then exit; raise exception.Create('CreateString.error'); end; function GetString(value:JsValueRef):string; var wptr : pwchar_t; len : size_t; t : JsValueType; begin result := ''; if JsGetValueType(value,t) = JsNoError then if t = JsString then if jsStringToPointer(value,wptr,len) = JsNoError then begin SetLength(result,len); move(wptr[0],result[1],len*sizeof(wchar_t)); exit; end; raise exception.Create('GetString.error'); end;
jsStringToPointer では文字列の先頭アドレスと文字の長さ(個数)を返します。なので、utf16ですからバイト長に変換するなら2 (sizeof(wchar_t)) をかける必要があります。
Delphiのmoveって気持ち悪いよね…。アドレスでは無く実体指定だから異質すぎる。
ユーザー定義オブジェクト型
いわゆるクラスとか構造体をぶち込みたいあなたの願いを叶えるものです。
luaにもありましたがChakraの指定はそれよりもシンプルで且つ分かりやすくなっています。
JsFinalizeCallback = procedure(data: Pointer); stdcall; function JsCreateExternalObject( data: Pointer; finalizeCallback: JsFinalizeCallback; var _object: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
ユーザー定義型(ポインタつっこむ型)はintじゃなく専用のを使って定義します。
作るときに解放処理コールバック関数つけちゃえばいいよね。いい…。luaはごにょごにょする必要があったのでとても有り難い仕様です。
procedure ObjectDispose_callback(data: Pointer); stdcall; begin TObject(data).Free; // call super class dispose Writeln( 'object.dispose:0x' + IntToHex(Integer(data),8) ); end; procedure MakeObject(var value:JsValueRef; obj:TObject); begin Writeln( 'object.entry:0x' + IntToHex(Integer(obj),8) ); if JsCreateExternalObject(pointer(obj),ObjectDispose_callback,value) = JsNoError then exit; raise exception.Create('CreateObject.error'); end; function GetObject(value:JsValueRef):TObject; begin // valuetype is JsObject result := nil; if JsGetExternalData(value,pointer(result)) = JsNoError then exit; raise exception.Create('GetObject.error'); end;
DelphiなのでクラスはすべてTObjectの派生という特性を利用してますが、このへんはおこのみで。
解放タイミング、ガベージコレクト
さて、ユーザー定義型でついて回るのがガベージコレクトによる解放タイミング。
luaは解放すると即 dispose 命令を発効してくれましたが、Chakraではそうはいきません。
オブジェクト生成、即解放するような処理でログを出してみると
object.create object.entry:0x023D8120 object.free get array[1]:45600 12300 45600 object.dispose:0x023D8120 // 解放コールバック関数が最後に呼ばれてる
ものの見事に最後でした。
一応ガベージコレクト全域に対してクリアする命令は存在します。
function JsCollectGarbage( runtime: JsRuntimeHandle ): JsErrorCode; stdcall; external DLL_NAME;
これで即時呼ばれると思ったのですが、ちょっと特性があるみたいです。
JsDeleteProperty -> 呼ばれない JsSetPropertyで値を上書き -> 呼ばれる
この挙動はよく分かりません。
関数の登録
変数が出来たので次は関数です。
Chakraの関数はクラスメソッドと同じで、すべて this が最初についた形で読んだり呼ばれたりします。
グローバルであれば、グローバルオブジェクトが this に入ります。
また、JavaScriptはthisの変更を許容しています。変えて良いんですね。変えませんけど…。
大本(c++)の定義を見てみると
CHAKRA_API JsCallFunction( _In_ JsValueRef function , _In_reads_(argumentCount) JsValueRef *arguments , _In_ unsigned short argumentCount , _Out_opt_ JsValueRef *result);
JsValueRef *arguments 、つまりarray(配列)とその個数指定することになっています。
なので、キャスト用構造体でも作っておくと便利です。
type TJsParamsRecord = packed record //packed _this : JsValueRef; values : array [0..31-1] of JsValueRef; //適当な個数です end; PJsParamsRecord = ^TJsParamsRecord;
ではまず関数を登録しましょう。関数はヘッダで宣言されている型と全く同じでないといけません。ちょっとでも違うとメモリ破壊が始まります。コワイ!
JsNativeFunction型がそれです。
type JsNativeFunction = function( callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer ): JsValueRef; stdcall;
arguments: PJsValueRef が最初に説明したthisを含む引数の配列です。nil/nullptrで終わってはいないのでちゃんと argumentCount で終端を限定する必要があります。
まずは print(コンソール出力) 的なものから作るのが世の常です。
// JS : print(s:string) // s : string // rreturn : none function implement_print(callee: JsValueRef; isConstructCall: bool; arguments: PJsValueRef; argumentCount: Word; callbackState: Pointer):JsValueRef; stdcall; var s : string; pParams : PJsParamsRecord; begin if argumentCount <> 2 then begin raise exception.Create('callback implement_printf.param error'); end; pParams := PJsParamsRecord(arguments); //cast s := GetString(pParams^.values[0]); Writeln(s); result := nil; end;
文字列引数を1つ持つため、this も足して引数カウントは2になります。
登録はいくつも行うので適当な関数を作って利用するといいでしょう。
procedure EntryFunction(obj : JsValueRef; const name:string; func:JsNativeFunction; state:pointer); var ref : JsValueRef; begin if jsCreateFunction(func,state,ref) = JsNoError then if SetProperty(obj,name,ref) then exit; raise exception.Create('EntryFunction.error'); end; ... EntryFunction(global,'imp_print',implement_print,nil);
JavaScript側からは以下のように使用できます。
imp_print"hello world :");
JavaScript側の関数をnative側から呼ぶ
この場合は jsCallFunction を使用します。
function JsCallFunction( _function: JsValueRef; arguments: PJsValueRef; argumentCount: short; var result: JsValueRef ): JsErrorCode; stdcall; external DLL_NAME;
関数を登録した際と同じように、引数は JsValueRef の配列で指定します。なので先ほど作った構造体を使用すると楽ちんです。
JavaScript側で定義した、文字列を表示させるグローバルなテスト関数を定義しておいて、それを呼ぶ場合は。
params : TJsParamsRecord; ref,subret : JsValueRef; ... // params params._this := global; // need "this." MakeString(ref,'sample message'); params.values[0] := ref; // call ref := GetProperty(global,'callback_test',JsFunction); jsCallFunction(ref,PJsValueRef(@params),2,subret); // callback_test('sample message');
callback_test('sample message'); という形でJavaScript上の関数が呼ばれます。
最後が返値のJsValueRefです。返さない場合もnilではない値が返ります。なんだろう?
さてこうしてみると、返値や、params.values[0] につっこんだ JsValueRef はちゃんと解放されるのか心配です。
リファレンスのRefによれば、「スタックに乗る限り自動的に解放されます」とのことです。なので返値や引数は大丈夫みたいです。
JsGetPropertyIdFromName で作った値なんかもスタックに乗るのかなどちょっと不安ですが、JsGetPropertyを通す以上乗ると思うのでたぶん大丈夫でしょう。
解放に関しては JsRelease という命令がありますが、基本は addRef 使用時に対で使うことぐらいしか触れられていません。
array配列操作
配列操作です。以下のようなものに対して操作します。
[1,2,5,6,8,9,100]
配列操作は JsGetProperty の配列版 JsGetIndexedProperty でおこないますが、インデックスの指定がintではなくJsValueRefになっています。
なので関数を作っておかないとちょっと面倒です。
procedure SetArray(_array:JsValueRef; index:integer; value:JsValueRef); var indexref : JsValueRef; begin if JsIntToNumber(index,indexref) = JsNoError then if JsSetIndexedProperty(_array,indexref,value) = JsNoError then exit; raise exception.Create('SetArray.error'); end; function GetArray(_array:JsValueRef; index:integer; _type:JsValueType):jsValueRef; var t : JsValueType; //len : integer; indexref : JsValueRef; begin result := nil; // length check //JsNumberToInt( GetProperty(_array,'length',JsValueNumber) , len ); //if (len >= index)or(index < 0) then //begin // raise exception.Create('GetArray.out of index'); //end; if JsIntToNumber(index,indexref) = JsNoError then if JsGetIndexedProperty(_array,indexref,result) = JsNoError then if JsGetValueType(result,t) = JsNoError then if t = _type then exit; raise exception.Create('GetArray.error'); end;
配列の長さ取得はjavascript上同様 length プロパティで出来るみたいです。
が、配列外を指定しても JsGetIndexedProperty がエラーを出してくれるので、ここでは必要なさそうです。
オブジェクト操作
もう今までで散々やってきたので、もういいよね。
オブジェクトの中身を一覧表示させたい場合はちょっと特殊な JsGetOwnPropertyNames を使用します。処理としてはpropertyの要素名の配列を取得して、あとはGetPropertyで回すだけ。
procedure PrintObject(obj:JsValueRef); var names : JsValueRef; namecount : integer; ref,idref : JsValueRef; t : JsValueType; d : double; i,n : integer; name,value : string; begin JsGetOwnPropertyNames(obj,names); // get propert name array object : ["aaa","bbb","cccc"] ref := GetProperty(names,'length',JsNumber); // get array count if ref = nil then exit; JsNumberToInt(ref,namecount); for i:=0 to namecount-1 do begin ref := GetArray(names,i,JsString); if ref = nil then continue; name := GetString(ref); if JsGetPropertyIdFromName(pwchar_t(name),idref) <> JsNoError then continue; if JsGetProperty(obj,idref,ref) <> JsNoError then continue; JsGetValueType(ref,t); value := ''; if t = JsString then begin value := GetString(ref); end; if t = JsNumber then begin JsNumberToDouble(ref,d); value := FloatToStr(d); end; WriteLn(' ' + name + ' : ' + value); end; end;