SpiderMonkey の判別とブラウザ判別2009年02月01日 18時30分

1 行でブラウザ判別を行うスクリプト (IE 用の日本語紹介記事Firefox、Safari 用の日本語紹介記事) が注目を集めています。それによると Firefox の判別は次の 15 文字で行えるそうです。

//Firefox detector 2/3 by DoctorDan
FF=/a/[-1]=='a'

いったいなぜこれで Firefox が判別できるのか、少し探ってみました。

  1. 何を判別しているのか
  2. SpiderMonkey のソースコードを探る
  3. より短い SpiderMonkey の判別法
  4. 「ブラウザ判別」としての不備
  5. まとめ
  6. 参考文献

何を判別しているのか

実際に探っていく前にひとつ注意しなければいけないことがあります。それは、上記のコードが Firefox を判別するものではないということです。このコードには Web ブラウザ固有のオブジェクトモデルに関する情報が含まれておらず、正確には Firefox 等で使われている JavaScript エンジン「SpiderMonkey」を判別するためのものであるというべきでしょう。

SpiderMonkey のソースコードを探る

上記コードを見るに、SpiderMonkey では正規表現オブジェクトのインデックス -1 が source プロパティに対応するようです。しかしそうなる原因を探るにはどうすればよいのでしょうか。とりあえず -1 プロパティを取得するという JavaScript コードに対して、SpiderMonkey が生成する中間コードを見てみることにします。中間コードは、単体の SpiderMonkey に付属する JavaScript shell で使える dis 関数で確認できます。なお、都合によりここで使用する SpiderMonkey 及び参照する SpiderMonkey のソースコードは、Firefox 3.0 系列に含まれるものと同等のバージョン (SpiderMonkey 1.8) のものとします。

$ js
js> dis(function () /a/[-1]);
flags: LAMBDA EXPR_CLOSURE INTERPRETED
main:
00000:  regexp null
00003:  int8 -1
00005:  getelem
00006:  return
00007:  stop

Source notes:
  0:     5 [   5] pcbase   offset 5

これを見ると、数値的プロパティの取得には getelem 命令が使われることがわかります。そこで、SpiderMoneky のソースコードから、getelem 命令を処理している部分を見ると次のようになっています (一部改変)。

/* jsinterp.c */
4534: BEGIN_CASE(JSOP_GETELEM)
4535:   /* Open-coded ELEMENT_OP optimized for strings and dense arrays. */
4536:   lval = FETCH_OPND(-2);
4537:   rval = FETCH_OPND(-1);
4538:   if (JSVAL_IS_STRING(lval) && JSVAL_IS_INT(rval)) {
            /* 文字列の数値的プロパティに関する処理 */
4548:   }
4549: 
4550:   VALUE_TO_OBJECT(cx, -2, lval, obj);
4551:   if (JSVAL_IS_INT(rval)) {
4552:       if (OBJ_IS_DENSE_ARRAY(cx, obj)) {
                /* 密な配列の数値的プロパティに関する処理 */
4566:       }
4567:       id = INT_JSVAL_TO_JSID(rval);
4568:   } else {
4569:       if (!js_InternNonIntElementId(cx, obj, rval, &id))
4570:           goto error;
4571:   }
4572: 
4573:   if (!OBJ_GET_PROPERTY(cx, obj, id, &rval))
4574:       goto error;
4575: end_getelem:
4576:   regs.sp--;
4577:   STORE_OPND(-1, rval);
4578: END_CASE(JSOP_GETELEM)

ひとつのブロックが BEGIN で始まって END で終わっているような気がしますが、これは C 言語のソースコードです。今の場合、lval には正規表現オブジェクト (へのポインタ)、rval には整数値 -1 が含まれることになります。lval、rval はともに jsval 型ですが、プロパティの操作に当たっては JSObject へのポインタ型と jsid 型の値を使うので、4550 行目で正規表現オブジェクト (へのポインタ) を JSObject へのポインタ型に、4567 行目で整数値を jsid 型に変換しています。

4573 行目のいざプロパティを取得する段では、取得結果を収める変数として rval を再利用しています。正規表現オブジェクトの場合、OBJ_GET_PROPERTY マクロによるプロパティの取得は js_GetProperty 関数 (JS_GetProperty 関数とは別物。大文字の JS から始まる関数は外部 API) を経て js_GetPropertyHelper 関数の呼び出しにつながります。

/* jsobj.c */
3644: JSBool
3645: js_GetPropertyHelper(JSContext *cx, JSObject *obj, jsid id, jsval *vp,
3646:                      JSPropCacheEntry **entryp)
3647: {
3648:     uint32 type;
3649:     int protoIndex;
3650:     JSObject *obj2;
3651:     JSProperty *prop;
3652:     JSScopeProperty *sprop;
3653: 
3654:     /* Convert string indices to integers if appropriate. */
3655:     CHECK_FOR_STRING_INDEX(id);
3656:     JS_COUNT_OPERATION(cx, JSOW_GET_PROPERTY);
3657: 
3658:     type = OBJ_SCOPE(obj)->shape;
3659:     protoIndex = js_LookupPropertyWithFlags(cx, obj, id, 0, &obj2, &prop);
3660:     if (protoIndex < 0)
3661:         return JS_FALSE;
3662:     if (!prop) {
3663:         jsbytecode *pc;
3664: 
3665:         *vp = JSVAL_VOID;
3666: 
3667:         if (!OBJ_GET_CLASS(cx, obj)->getProperty(cx, obj, ID_TO_VALUE(id), vp))
3668:             return JS_FALSE;
3669: 
              /* プロパティキャッシュに関する処理 */
              /* 最終的にプロパティが存在しなかった場合の処理 */
3716:         return JS_TRUE;
3717:     }
3718: 
          /* すでにプロパティが存在する場合の処理 */
          /* プロパティキャッシュに関する処理 */
3733:     return JS_TRUE;
3734: }

3659 行目でプロパティの探索を行いますが、-1 という名前のプロパティは本来正規表現オブジェクトに存在しないので見つからず、変数 prop の値は NULL になります。すると、3667 行目で、正規表現オブジェクトに共通するプロパティ取得時用フック関数 (regexp_getProperty 関数) が、整数 -1 (を表す jsval 型の値) とともに呼び出されるのです。

/* jsregexp.c */
3595: enum regexp_tinyid {
3596:     REGEXP_SOURCE       = -1,
3597:     REGEXP_GLOBAL       = -2,
3598:     REGEXP_IGNORE_CASE  = -3,
3599:     REGEXP_LAST_INDEX   = -4,
3600:     REGEXP_MULTILINE    = -5,
3601:     REGEXP_STICKY       = -6
3602: };

3607: static JSPropertySpec regexp_props[] = {
3608:     {"source",     REGEXP_SOURCE,      RO_REGEXP_PROP_ATTRS,0,0},
3609:     {"global",     REGEXP_GLOBAL,      RO_REGEXP_PROP_ATTRS,0,0},
3610:     {"ignoreCase", REGEXP_IGNORE_CASE, RO_REGEXP_PROP_ATTRS,0,0},
3611:     {"lastIndex",  REGEXP_LAST_INDEX,  REGEXP_PROP_ATTRS,0,0},
3612:     {"multiline",  REGEXP_MULTILINE,   RO_REGEXP_PROP_ATTRS,0,0},
3613:     {"sticky",     REGEXP_STICKY,      RO_REGEXP_PROP_ATTRS,0,0},
3614:     {0,0,0,0,0}
3615: };
3616: 
3617: static JSBool
3618: regexp_getProperty(JSContext *cx, JSObject *obj, jsval id, jsval *vp)
3619: {
3620:     jsint slot;
3621:     JSRegExp *re;
3622: 
          /* 前処理 */
3630:     slot = JSVAL_TO_INT(id);
3631:     if (slot == REGEXP_LAST_INDEX)
3632:         return JS_GetReservedSlot(cx, obj, 0, vp);
3633: 
3634:     JS_LOCK_OBJ(cx, obj);
3635:     re = (JSRegExp *) JS_GetPrivate(cx, obj);
3636:     if (re) {
3637:         switch (slot) {
3638:           case REGEXP_SOURCE:
3639:             *vp = STRING_TO_JSVAL(re->source);
3640:             break;
3641:           case REGEXP_GLOBAL:
3642:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_GLOB) != 0);
3643:             break;
3644:           case REGEXP_IGNORE_CASE:
3645:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_FOLD) != 0);
3646:             break;
3647:           case REGEXP_MULTILINE:
3648:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_MULTILINE) != 0);
3649:             break;
3650:           case REGEXP_STICKY:
3651:             *vp = BOOLEAN_TO_JSVAL((re->flags & JSREG_STICKY) != 0);
3652:             break;
3653:         }
3654:     }
3655:     JS_UNLOCK_OBJ(cx, obj);
3656:     return JS_TRUE;
3657: }

regexp_getProperty 関数内では、3630 行目で変数 slot に id の値である -1 が収められます。3638 行目でそれが定数 REGEXP_SOURCE と一致することで、-1 というプロパティ名に対し source プロパティと同じ値が返ってくるのです。

source に対する -1 のような、プロパティを表す整数を tiny id と呼びます。SpiderMonkey では、プロパティごとにゲッタ / セッタ用フック関数を設定するのではなく、特定のオブジェクトの全プロパティに共通するゲッタ / セッタ用フック関数を使うと、JavaScript コードからの tiny id によるアクセスがプロパティ名によるアクセスと同一視されてしまうそうです。

より短い SpiderMonkey の判別法

外部から tiny id を使ってプロパティにアクセスできるのは正規表現オブジェクトに限りません。文字列の場合は length プロパティに対する tiny id として -1 が割り当てられ、それにより外部から参照できます。これを使えば、結果が真偽値となるような SpiderMonkey の判別は次の 12 文字で可能です。

SM=""[-1]==0

「ブラウザ判別」としての不備

最初に紹介したページに掲載されている判別法は、いずれもブラウザの JavaScript エンジンの差異を用いたものです。これらは JavaScript エンジンの内部実装に大きく依存するものであり、エンジンのちょっとした変更で期待した動作をしなくなる可能性も否定できません。実際、そこで Firefox 3 の判別用と紹介されている以下のコードの結果は、Firefox 3.1 では false となります。SpiderMonkey は現在、関数オブジェクトでは古びて非効率的な方法を捨て、プロパティごとにゲッタ用フック関数を指定するようにしたからです。

//Firefox 3 by me:-
FF3=(function x(){})[-5]=='x'

また、一般にブラウザによってスクリプトの動作が違うといったとき、その原因がスクリプトエンジンであることはあまりありません。多くの場合はレイアウトエンジン (ここでは DOM 等オブジェクトモデルの提供もレイアウトエンジンの役割に含めます) の動作の違いによるものです。Safari と Google Chrome はともにレイアウトエンジンに WebCore を用いていますが、スクリプトエンジンはそれぞれ JavaScriptCore と V8 と異なっています。WebCore に起因する Safari の問題を回避するのに JavaScriptCore の判別を使っていれば、Google Chrome が同じ問題を持っていたときに対処できません。

特定ブラウザでの問題に対処するときは、プロパティやメソッドの存在確認や動作確認により処理を切り分けるべきです。それで判定できないときはブラウザ判別を行うことになりますが、そこで調べるのはブラウザではなくブラウザのレイアウトエンジンです。navigator.userAgent プロパティから調べるのなら、"Firefox" や "Safari" ではなく "Gecko/" や "WebKit" といった文字列の有無で判断しましょう。いずれにしても、問題がレイアウトエンジンにあるのなら、上記ページで紹介されているようなスクリプトエンジンに基づくブラウザ判別を使ってはいけません

まとめ

  • SpiderMonkey では、プロパティを表す tiny id によりプロパティにアクセスできることがあります。
  • 問題解決のためのブラウザ判別にはプロパティ / メソッドの存在確認や動作確認を用いるか、それが無理ならレイアウトエンジンを調べましょう。
  • スクリプトエンジンの判別はレイアウトエンジンの判別になりません。

参考文献

コメント

コメントをどうぞ

※メールアドレスとURLの入力は必須ではありません。 入力されたメールアドレスは記事に反映されず、ブログの管理者のみが参照できます。

※投稿には管理者が設定した質問に答える必要があります。

名前:
メールアドレス:
URL:
次の質問に答えてください:
「ハイパーテキストマークアップ言語」をアルファベット4文字でいうと?

コメント:

トラックバック

このエントリのトラックバックURL: http://nanto.asablo.jp/blog/2009/02/01/4095132/tb