nazonoRubyist RubyでJavaScriptのためにC このページをアンテナに追加 RSSフィード

2006-06-28

例外処理 12:06 例外処理 - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - 例外処理 - nazonoRubyist RubyでJavaScriptのためにC 例外処理 - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

さて、昨日のコードであるが。

まず驚くべきは、コールバック関数引数-2 argv[-2] で関数オブジェクトが取り出せる!って何だよこの仕様オブジェクト指向では一般的なの? PerlJavaScript::SpiderMonkeyソースで使用されているのを知って初めて知ったのだが…-1に何が入っているかは調べていません。

で、取り出した関数オブジェクトから、事前に仕込んでおいたプロパティーを取り出して、そこから procオブジェクトを取り出している。あ、このへんエラー処理してないな。

その後、それらを実行するわけだが、普通proc を実行してしまうと、proc 内で例外が投げられたときにセグメンテーション違反で落ちてしまう(ということで数日悩んだ)。対策としては、rb_protect 内で、procを呼び出す。rb_protect で呼び出される関数は、引数を一個しか受け取らないので*1、args の最後にprocpushしている。

rb_smjs_ruby_proc_callerの内部はとくにおかしなところはなく

// protect 内で proc を呼ぶ。args の最後に proc が入っている
static VALUE
rb_smjs_ruby_proc_caller(VALUE args){
  // Procを実行
  VALUE proc = rb_ary_pop( args );
  return rb_apply(proc, rb_intern("call"), args);
}

こんなかんじ。

さて、これで終わってしまうと、Rubyproc内で投げられた例外は、すべて消失してしまう。投げられた例外を再びRuby世界に戻すためには rb_jump_tag( status ) を呼び出すことが必要だ。しかし、それを実行するのは、この「JavaScriptエンジンから呼び出されたC関数」の中であってはいけない。ではどこか、というと、RubyからJavaScriptエンジンに制御を移した場所でないといけない。

世界はこうなっているわけだ。

Ruby >> (例外が)越えられない壁=JavaScript >> Ruby

現在のところ、eval にしか RubyJavaScript の境界面はなく、rb_smjs_value_function_callback にしか JavaScriptRuby の境界面はない。つまり、正しい解決方法は rb_smjs_value_function_callback で発生するRuby例外はJavaScript例外にラップして JavaScript世界に投げ、evalJavaScript例外をキャッチしてRuby例外にしてRuby世界に投げる、という実装だろう。つまりそれを実装しているのが rb_smjs_raise_js である。

// 最後に発生したエラーの情報
struct sSMJS_Error{
  int status;
}gSMJS_LastError;

JSBool
rb_smjs_raise_js( JSContext *cx, int status ){
  SMJS_LastError.status = status;
  JSObject* exp = JS_NewObject( cx, NULL, NULL, NULL );
  JS_SetPendingException( cx, OBJECT_TO_JSVAL(exp) );
  return JS_FALSE;
}

えへへ、だが眠かったので、例外をグローバル変数に保存しておくようにしちゃった。これでスレッド・アンセーフになったわけだが、気にせずいこう。この辺は JavaScript内でクラスが実装できるようになってから変更することにする。

で、対応する JS_EvaluateScript を呼んでいる部分は

  JSBool ok = JS_EvaluateScript( cs->cx, cs->globalObj, source, strlen(source),
      filename, lineno, &value);
  if (!ok){
    rb_jump_tag( gSMJS_LastError.stat );
  }

こんなかんじ。まあ、このままだと「Ruby例外以外の例外で終了した場合」にまずいわけだが、その辺は実際の実装を参考に。

*1:実はポインタなので適当にだますことは可能なのだが

トラックバック - http://rubyist.g.hatena.ne.jp/nazoking/20060628

2006-06-27

Windowsで動かしている人が! Windowsで動かしている人が! - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - Windowsで動かしている人が! - nazonoRubyist RubyでJavaScriptのためにC Windowsで動かしている人が! - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

http://rubyist.g.hatena.ne.jp/miyamuko/20060627/

tmp\ruby-smjs>ruby test.rb
Loaded suite test
Started
......E......E
Finished in 0.188 seconds.

  1) Error:
test_defclass(SpiderMonkeyTest):
NoMethodError: undefined method `bindClass' for #<SpiderMonkey::Context:0x2971f1
8>
    test.rb:188:in `test_defclass'

  2) Error:
test_wikiparser(SpiderMonkeyTest):
Errno::ENOENT: No such file or directory - WikiParser.js
    test.rb:16:in `initialize'
    test.rb:16:in `test_wikiparser'

14 tests, 65 assertions, 0 failures, 2 errors

おお! 動いてますね。テストの通ってないのは僕の手元と同じで、仕様です(汗)。WikiParser.js がないのはおなじディレクトリ

 wget http://dev.ishinao.net/WikiParser/javascript/WikiParser.js

とでもすれば、多分大丈夫。

Mingwの方はよくわかりません! 終了処理で失敗しているのか?JS_DestroyRuntimeが呼ばれない、some situation?それとも、JSContextにSetContextPrivateでルビーオブジェクトを結びつけているのが悪いのかな?


Ruby/SpiderMonkeyビルド

VC++ だと大量のエラー (それも理不尽な) が出るので以下のパッチを当てる。

エラーの原因は以下の通り。

パッチは手作業で(汗)取り込みました。コメントは最後に半角スペースを入れることにしたので、多分そのままで通るようになったと思います。手元の環境だと _alloca が失敗したので、jargv[argc-1] はjargv[argc]と一個多めに確保することにしました(--;)

ありがとうございます


しかしいつ駄目になるかはわからない罠…コンパイルオプションか何かでチェックできればいいんだけど。

関数の設定と実行 13:48 関数の設定と実行 - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - 関数の設定と実行 - nazonoRubyist RubyでJavaScriptのためにC 関数の設定と実行 - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

  v = SpiderMonkey::eval("v={}")
  v.function :hoge, { |a|
    a + 1
  }
  assert_equal 3, v.call(:hoge, 2 )

こんな感じのインタフェースで動くようにしよう。

関数を定義する関数

// Ruby関数をJavaScriptに登録する
VALUE
rb_smjs_value_function( int argc, VALUE *argv, VALUE self ){
  // 引数の解析
  VALUE proc, name;
  rb_scan_args(argc, argv, "1&", &name, &proc );
  jsval value=RBSMValue_TO_JSVAL( self );
  JSContext *cx= RBSMValue_TO_JsContext( self );
  char *cname = StringValuePtr(name);

  // 関数をJS側のオブジェクトに設定し、呼ばれたら rb_smjs_value_function_callback が
  // 呼ばれるようにする
  JSFunction* fun = JS_DefineFunction( cx, 
      JSVAL_TO_OBJECT(value), cname, 
      rb_smjs_value_function_callback, 0, 0 );
  if(!fun) rb_raise( eJSError,"js define function" );

  // 関数のプロパティーに、procへのポインタを持つオブジェクトを設定
  JSObject* fobj=JS_GetFunctionObject(fun);
  JSObject *pobj = JS_NewObject( cx, NULL, NULL, fobj );
  JS_SetPrivate( cx, pobj, (void *)proc );
  JS_DefineProperty( cx, fobj, "__ruby_funcion_", OBJECT_TO_JSVAL(pobj),
      NULL,NULL,JSPROP_PERMANENT | JSPROP_READONLY );

  return proc;
}

JSObjectにはJS_SetPrivateで自由にvoid*型の(つまりなんでも)ポインタを設定できる。で、関数オブジェクトに設定しようとしたところ、なぜかセグメンテーション違反が出てしまった。ので、関数オブジェクトに謎のプロパティーを設定し、そのプロパティー=JSObjectに、procへのポインタを渡すようにした。これって、GCとかでprocが消えちゃったり移動したりしないことが前提の設定だなぁ。まあいい。

これによって、設定した関数JavaScriptから呼び出されたときは rb_smjs_value_function_callback が呼び出される。その実装はこうなっている。

// 登録したRuby関数がJavaScriptから呼ばれた
static JSBool
rb_smjs_value_function_callback(
    JSContext *cx, JSObject *thisobj, 
    uintN argc, jsval *argv, 
    jsval *rval){
  // 呼び出された関数をオブジェクトとして取り出す
  JSFunction *fun = JS_ValueToFunction( cx, argv[-2]);
  JSObject *fobj = JS_GetFunctionObject(fun);

  // 関数オブジェクトに仕込んでおいたプロパティーからProcを取り出す
  jsval rfuncval;
  JS_GetProperty( cx, fobj, "__ruby_funcion_", &rfuncval);
  JSObject *obj = JSVAL_TO_OBJECT(rfuncval);
  VALUE proc = (VALUE)JS_GetPrivate(cx,obj);

  // JSContextに仕込んでおいたプライベート値からContextを取り出す
  VALUE context = (VALUE)JS_GetContextPrivate(cx);

  // 引数をSpiderMonkey::Valueに。
  // 引数の最後にprocをくっつける
  VALUE rargs = rb_ary_new2( argc + 1);
  uintN i;
  for( i=0; i<argc; i++ ){
    rb_ary_store( rargs, i, rb_smjs_value_new_jsval( context, argv[i] ) );
  }
  rb_ary_store( rargs, i, proc );

  // proc を実行
  int status;
  VALUE res = rb_protect( rb_smjs_ruby_proc_caller, rargs, &status );

  // 例外が投げられた、等の場合
  if( status != 0){
    return rb_smjs_raise_js( cx, status );
  }

  // 返値をJavaScriptオブジェクトに変換
  return rb_smjs_ruby_to_js( cx, res, rval );
}

いくつかポイントが。あるが、それはまた明日。

2006-06-26

JS_SetErrorReporter 14:44 JS_SetErrorReporter - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - JS_SetErrorReporter - nazonoRubyist RubyでJavaScriptのためにC JS_SetErrorReporter - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

エラーメッセージの取得である。

SpiderMonkeyにはJS_SetErrorReporterというAPIが用意されているので、これで設定するのだろう。

//エラーレポート
void
rb_smjs_context_errorhandle(
    JSContext *cx, 
    const char *message, 
    JSErrorReport *report){
  rb_raise(eJSEvalError,"%s:%d:%s", (report->filename ? report->filename : "NULL"), report->lineno, message );
}


static VALUE
rb_smjs_context_initialize(VALUE self){
  :
  // エラーレポーターを設定
  JS_SetErrorReporter(cs->cx, rb_smjs_context_errorhandle );
  
  return Qnil;
}

とりあえずこんな感じにしたところ、

irb(main):003:0> SpiderMonkey::eval(" hoge'")
SpiderMonkey::EvalError: [null]:1:SyntaxError: unterminated string literal
        from (irb):3:in `eval'
        from (irb):3

お、上手くエラーがつかめているようだ。今まではどこで何のエラーが出ているのかわからなかったのだ。上のエラーだと、1行目で SyntaxErrorが出ている、ということまでわかる。

本当ならSpiderMonkey::SyntaxError などを設定すべきだが、当面はこれで。エラーの種類のつかみ方がよくわからないのもある。


で、これで見たところ、WikiParserが動かないのは htmlescape という関数がない、ということのようだ。htmlescape だから多分

function htmlencode(txt){ 
  return txt.replace(/\&/g,'&amp;').replace(/\</g,'&lt;')
    .replace(/>/g,'&gt').replace(/"/g,'&quot;')
}

こんなかんじのはず。で、実行してみると動いた!感動


しかし、ErrorReporter から rb_raise しちゃっていいのだろうか?なんかよくない気がするなぁ…

(1)Ruby → (2)SpiderMonkey::eval → (3)JS_ScriptEvaluate → (4)Javascriptインタプリタ → (5)実行(複雑にスタック積んだり) → (6)エラー発生 → (7)rb_raise

すると、(7)からJavaScriptインタプリタ内での処理をすべて無視して (1)まで戻ってしまう… (7)では、エラー内容をラップしたJavaScriptの例外をthrowし、(3)でキャッチして処理が戻った後で rb_raise すべきだろう。しかしJavaScriptの例外をCで投げる方法・受ける方法がわからない上にエラーレポーターで投げていいのかどうかもわからないので、後回しに…

あ、そうか、受けられれば、投げるのはいらないんだ…でも受け方がわからないので後回し

トラックバック - http://rubyist.g.hatena.ne.jp/nazoking/20060626

2006-06-25

jsval (Javascript値)を VALUE (Rubyオブジェクト)に変換 14:27 jsval (Javascript値)を VALUE (Rubyオブジェクト)に変換 - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - jsval (Javascript値)を VALUE (Rubyオブジェクト)に変換 - nazonoRubyist RubyでJavaScriptのためにC jsval (Javascript値)を VALUE (Rubyオブジェクト)に変換 - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

さて、いよいよこれができるとようやく Ruby/JS と同程度の機能がそろう。そう、この部分は最初の日に作ったのであった。が、コードを載せていなかったので、ここに書く。

JavaScriptでインタプリタを実行するJS_EvaluateScriptは、結果値を jsval という型の値に代入して返してくれる。SpiderMonkeyのリファレンスでは、このjsval と、コンテキストの状態であるJSContextからCの値を取り出す方法がいろいろと書かれているのだが、我々がほしいのは当然Rubyの値である。Rubyの値はVALUEという型で表現する。

最初に作ったのはこの部分だ。今まで何度か出てきていた、rb_smjs_convertvalueである。

VALUE
rb_smjs_convertvalue(JSContext *cx, jsval value){
  JSType t = JS_TypeOfValue(cx,  value);
  switch( t ){
    case JSTYPE_VOID: return Qnil;
    case JSTYPE_STRING:  return rb_smjs_to_s( cx, value );
    case JSTYPE_BOOLEAN: return rb_smjs_to_bool( cx, value );
    case JSTYPE_OBJECT:
      if( JSVAL_IS_NULL(value) ) return Qnil;
      if( JS_IsArrayObject(cx, JSVAL_TO_OBJECT(value)) )
        return rb_smjs_to_a(cx, value);
      return rb_smjs_to_h(cx,value);
    case JSTYPE_NUMBER:
      return rb_smjs_to_num(cx,value);
    case JSTYPE_FUNCTION:
      rb_raise (eJSConvertError, "function no support"); break;
    default:
      rb_raise (eJSConvertError, "object not supported type");
      break;
  }
}

で、そこで振り分けられた後に実際に変換する部分はこうなる

// Stringに変換
VALUE
rb_smjs_to_s(JSContext *cx, jsval value){
  JSString *str = JS_ValueToString( cx, value );
  return rb_str_new( JS_GetStringBytes( str ), JS_GetStringLength( str ) )
;
}

//Bool型に変換
VALUE
rb_smjs_to_bool( JSContext *cx, jsval value ){
  JSBool bp;
  if( !JS_ValueToBoolean( cx, value, &bp) ){
    rb_raise (eJSConvertError, "can't convert to boolean");
  }
  return bp ? Qtrue : Qfalse;
}
//Array型に変換
VALUE
rb_smjs_to_a(JSContext *cx, jsval value){
  VALUE ary;
  JSObject *o=JSVAL_TO_OBJECT(value);
  if( JSVAL_IS_OBJECT(value) ){
    if( JSVAL_IS_NULL(value) ) return rb_ary_new();
    if( JS_IsArrayObject(cx, o) ){
      jsuint length;
      if( JS_HasArrayLength(cx, o, &length) ){
        ary = rb_ary_new2( length );
        jsuint i;
        for( i=0; i<length; i++){
          jsval v;
          if( JS_GetElement( cx, o, i, &v ) ){
            rb_ary_store(ary, i, rb_smjs_convertvalue( cx, v ) );
          }else{
            rb_raise (eJSConvertError, "can't convert to array[]");
          }
        }
        return ary;
      }
    }
  }
  // うまく変換できなかったら、適当なRuby型に変換した後にその to_a メソッドを呼ぶ
  VALUE r = rb_smjs_convertvalue( cx, value);
  return rb_funcall( r, rb_intern("to_a"),0 );
}

// Hash型に変換
VALUE
rb_smjs_to_h(JSContext *cx, jsval value){
  if( JSVAL_IS_OBJECT(value) && !JSVAL_IS_NULL(value) ){
    VALUE ret = rb_hash_new();
    JSIdArray *ida = JS_Enumerate(cx, JSVAL_TO_OBJECT(value));
    if(!ida){
      rb_raise (eJSConvertError, "can't enumerate");
      return ret;
    }

    int i;
    for (i = 0; i < ida->length; i++) {
      jsval key,val;
      jsint id= ida->vector[i];
      if(JS_IdToValue(cx, id, &key)){
        char *name = JS_GetStringBytes(JSVAL_TO_STRING (key));
        if(JS_GetProperty(cx, JSVAL_TO_OBJECT(value), name, &val )){
          rb_hash_aset( ret, rb_smjs_convertvalue(cx, key) , rb_smjs_convertvalue(cx,val) );
        }else{
          rb_raise (eJSConvertError, "can't get property");
          break;
        }
      }else{
        rb_raise (eJSConvertError, "can't get key name");
        break;
      }
    }

    JS_DestroyIdArray(cx, ida);
    return ret;
  }
  rb_raise (eJSConvertError, "can't convert to hash from no object or null");
}
//Integer型に変換
VALUE
rb_smjs_to_i(JSContext *cx, jsval value){
  jsint ip;
  if( JS_ValueToInt32(cx, value, &ip) ){
    return INT2NUM( ip );
  }else{
    rb_raise (eJSConvertError, "can't convert object to i");
  }
}

//Float型に変換
VALUE
rb_smjs_to_f(JSContext *cx, jsval value){
  jsdouble d;
  if( JS_ValueToNumber( cx, value, &d) ){
    return rb_float_new( d );
  }else{
    rb_raise (eJSConvertError, "can't convert object to float");
  }
}

//整数ならInteger、そうでない場合はFloatに変換
VALUE
rb_smjs_to_num(JSContext *cx, jsval value){
  if( JSVAL_IS_INT(value) ){
    return rb_smjs_to_i(cx, value);
  }else if(JSVAL_IS_DOUBLE(value)){
    return rb_smjs_to_f(cx, value);
  }else{
    rb_raise (eJSConvertError, "can't convert to num");
  }
}

うむ。後はSpiderMonkey::Valueにメソッドとして登録してやるだけだ。

  rb_define_method( cJSValue, "to_ruby", rb_smjs_value_to_ruby, 0);
  rb_define_method( cJSValue, "to_s", rb_smjs_value_to_s, 0 );
  rb_define_method( cJSValue, "to_a", rb_smjs_value_to_a, 0 );
  rb_define_method( cJSValue, "to_h", rb_smjs_value_to_h, 0 );
  rb_define_method( cJSValue, "to_f", rb_smjs_value_to_f, 0 );
  rb_define_method( cJSValue, "to_i", rb_smjs_value_to_i, 0 );
  rb_define_method( cJSValue, "to_num", rb_smjs_value_to_num, 0 );
  rb_define_method( cJSValue, "to_bool", rb_smjs_value_to_bool, 0 );

evalutate 14:27 evalutate - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - evalutate - nazonoRubyist RubyでJavaScriptのためにC evalutate - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

さらに、Contextクラスにevalutateメソッドを追加した。

VALUE
rb_smjs_context_evaluate(VALUE self, VALUE code){^
  return rb_funcall( rb_smjs_context_eval(self, code), rb_intern( "to_ruby"), 0 );
}

これで

 r = context.eval(%| javascript |)

とすると、JavaScriptをラップしたSpiderMonkey::Valueが、

 r = context.evaluate(%| javascript |)

とすると、r にはRuby型が返るようになった。

同じ名前をSpiderMonkeyの特異メソッドとしても定義し、こちらは@@defaultContextのメソッドを呼ぶようにもしてやる。

VALUE
rb_smjs_eval( VALUE self, VALUE code ){
  VALUE cont = rb_iv_get(self, RBSMJS_DEFAULT_CONTEXT );
  return rb_smjs_context_eval( cont, code );
}

VALUE
rb_smjs_evaluate( VALUE self, VALUE code ){
  VALUE ret = rb_smjs_eval( self, code );
  return rb_smjs_value_to_ruby( ret );
}

とりあえず目的としていたメソッドはこれでおおよそ片が付いたはずだ。

今後の方針 14:27 今後の方針 - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - 今後の方針 - nazonoRubyist RubyでJavaScriptのためにC 今後の方針 - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

  • Eval時のエラーの詳細を知りたい
  • RubyのブロックをJavascriptから関数として呼び出したい
  • Rubyのクラスを登録してJavascript内で使いたい

他にも色々あるけど、当面これで。

トラックバック - http://rubyist.g.hatena.ne.jp/nazoking/20060625

2006-06-24

ナイトリー 14:46 ナイトリー - nazonoRubyist RubyでJavaScriptのためにC を含むブックマーク はてなブックマーク - ナイトリー - nazonoRubyist RubyでJavaScriptのためにC ナイトリー - nazonoRubyist RubyでJavaScriptのためにC のブックマークコメント

ところで、現在制作中のソースコードは以下のアドレスダウンロード可能です。

http://nazo.yi.org/rubysmjs/ruby-smjs.tar.gz

ナイトリー扱いなので、ビルドできないかもしれません。日記は、解決した問題を順々に書いているので、成果物と状態があわないことが往々にしてあります。とくに、test.rb は、「そのテストが走るように鋭意開発中」なので、通らないことの方が多いです。

ビルドの仕方

wget http://nazo.yi.org/rubysmjs/ruby-smjs.tar.gz
tar zxf ruby-smjs.tar.gz
cd ruby-smjs
ruby extconf.rb
make
ruby test.rb
make install (オススメできない)

debian/sarge Ruby1.8でしか試していません。Windowsでは動かないので、誰か動くようにしてください…他の部分もびしばし修正してください。nazoking@gmail.com まで。

secondlifesecondlife2006/06/24 17:58debian/sid でも動きました!!

nazokingnazoking2006/06/25 00:19おお早速。ありがとうございます!

bongolebongole2006/06/25 05:40freebsd6/ppc でも動きました!

nazokingnazoking2006/06/25 14:25まじですか>freebsd/ppc
CPANのJavaScript::SpiderMonkeyやPython-SpiderMonkeyのコードを読んでたら、ライブラリ名が「js」になってるので、debian以外ではそうなのかも、とドキドキしていたのですが。恐るべきRuby

bongolebongole2006/06/25 21:59あ、すいません。smjsはjsに直させていただきました。
でもヘッダの位置とライブラリの名前が違うだけであとは動きましたよー;-)

nazokingnazoking2006/06/26 14:47やっぱりライブラリとヘッダの位置が違うんですね… そういうのってextconf で調整するものなのかな?

トラックバック - http://rubyist.g.hatena.ne.jp/nazoking/20060624