2016年3月19日

VisualStudio と offsetof と参照型 / VisualStudio and offsetof and reference types

Windows 環境でのみ発生するクラッシュに 1 日ほど悩まされたので、その原因について。

I got troubled by a Windows-only crash and spent 1 day. Here's the reason of the crash.

offsetof ってどうコンパイルされんの? / How does offsetof get compiled?

とりあえずは公式のドキュメントを見ましょう。 offsetof Macro によると、以下のようにオフセットを返すという事くらいしか書かれていません。

Let's check the official document as a first step. offsetof Macro says only that it returns the offset.

The offsetof macro returns the offset in bytes of memberName from the beginning of the structure specified by structName as a value of type size_t. You can specify types with the struct keyword.

これでは何も分からないので、実際にコンパイルして出てきたコードを見てみましょう。

It doesn't help much, so let's check out the generated code.

こちらが今回使用するコードです。

Here's the source code for testing.

// offsetof_ptr.cpp

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

class C {
  int32_t* a;
  int32_t* b;

public:
  static size_t offsetOfB() {
    return offsetof(C, b);
  }
};

int
main(void) {
  fprintf(stderr, "%d\n", C::offsetOfB());
  return 0;
}

C::offsetOfB メソッドは、C クラスの b メンバのオフセットを返してくれるハズです。コンパイルには VisualStudio 2013 Community の cl コマンドを使用します。

C::offsetOfB method should return the offset of b member of C class. I use cl command of VisualStudio 2013 Community to compile it.

E:\> cl offsetof_ptr.cpp
E:\> offsetof_ptr.exe
4

返してくれました。では、どういう仕組みで計算してるのでしょうか。

It does. So, how does it calculate the offset?

E:\> cl /P offsetof_ptr.cpp
E:\> cl /FA offsetof_ptr.cpp

まずはプリプロセスの結果。

First, here's the result of preprocessing.

// offsetof_ptr.i with VisualStudio 2013, code for offsetOfB

  static size_t offsetOfB() {
    return (size_t)&reinterpret_cast<const volatile char&>((((C *)0)->b));

offsetof が書き変わっています。という事で、マクロの類いである事が分かります。

offsetof is replaced. It means that it would be a kind of macro.

次にアセンブリでは、即値として 4 を返しています。

Next, in assembly, it returns an immediate value 4.

; offsetof_ptr.asm with VisualStudio 2013, assembly for C::offsetOfB

; Function compile flags: /Odtp
;       COMDAT ?offsetOfB@C@@SAIXZ
_TEXT   SEGMENT
?offsetOfB@C@@SAIXZ PROC                                ; C::offsetOfB, COMDAT
; File e:\offsetof_ptr.cpp
; Line 10
        push    ebp
        mov     ebp, esp
; Line 11
        mov     eax, 4
; Line 12
        pop     ebp
        ret     0
?offsetOfB@C@@SAIXZ ENDP                                ; C::offsetOfB
_TEXT   ENDS

参照型と一緒に使うとどうなる? / What happens if I use it with reference types?

問題はこの offsetof を参照型のメンバに使用するとどうなるか、という事です。offsetof_ptr.cpp から、b の型をポインタから参照に変えてみましょう。

The problem is, what happens if I use offsetof to a reference-typed member. Let's change the type of b from a pointer to a refernce, in offsetof_ptr.cpp.

// offsetof_ref.cpp

#include <stdio.h>
#include <stddef.h>
#include <stdint.h>

class C {
  int32_t* a;
  int32_t& b;

public:
  static size_t offsetOfB() {
    return offsetof(C, b);
  }
};

int
main(void) {
  fprintf(stderr, "%d\n", C::offsetOfB());
  return 0;
}

とりあえず実行してみます。

Let's give it a try.

E:\> cl offsetof_ref.cpp
E:\> offsetof_ref.exe

クラッシュしました。

it crashes.

という事で、生成されたコードを見てみましょう。

So, let's check the generated code.

E:\> cl /P offsetof_ref.cpp
E:\> cl /FA offsetof_ref.cpp
// offsetof_ref.i with VisualStudio 2013, code for offsetOfB

  static size_t offsetOfB() {
    return (size_t)&reinterpret_cast<const volatile char&>((((C *)0)->b));
  }

offsetof の変換結果に差がありません。

There is no difference in the conversion result of offsetof.

    return (size_t)&reinterpret_cast<const volatile char&>((((C *)0)->b));

これでは、メンバのアドレスではなくメンバの値を返しています。

With this way, it returns the value of the member, not the address of the member.

念のため、アセンブリでも見てみましょう

Just to be sure, let's check the assembly.

; offsetof_ref.asm with VisualStudio 2013, assembly for C::offsetOfB

; Function compile flags: /Odtp
;       COMDAT ?offsetOfB@C@@SAIXZ
_TEXT   SEGMENT
?offsetOfB@C@@SAIXZ PROC                                ; C::offsetOfB, COMDAT
; File e:\offsetof_ref.cpp
; Line 10
        push    ebp
        mov     ebp, esp
; Line 11
        mov     eax, 4
        mov     eax, DWORD PTR [eax]
; Line 12
        pop     ebp
        ret     0
?offsetOfB@C@@SAIXZ ENDP                                ; C::offsetOfB
_TEXT   ENDS

明かに 4 をデリファレンスしてくれてます。

It clearly dereferences 4.

        mov     eax, 4
        mov     eax, DWORD PTR [eax]

生成されたコードは Visual Studio 2015 Community でも全く同じでした。

The generated code is exactly same for Visual Studio 2015 Community too.

結論としては、offsetof は参照型のメンバに使えない、使うとクラッシュする、という事でした。

So, as a conclusion, offsetof cannot be used to a reference-typed member, and if you use, it crashes.

clang と gcc はどうやってんの? / How do clang and gcc handle this?

こちらは clang++ のプリプロセス結果とアセンブリ。

This is the result of preprocess and the assembly with clang++.

// offsetof_ref.ii with clang++ 700.1.81, code for offsetOfB

  static size_t offsetOfB() {
    return __builtin_offsetof(C, b);
  }
; offsetof_ref.s with clang++ 700.1.81, assembly for C::offsetOfB

__ZN1C9offsetOfBEv:                     ## @_ZN1C9offsetOfBEv
## BB#0:
        pushl   %ebp
        movl    %esp, %ebp
        movl    $4, %eax
        popl    %ebp
        retl

こちらは g++ のプリプロセス結果とアセンブリ。

and this is the result of preprocess and the assembly with g++.

// offsetof_ref.ii with g++ 4.9.2, code for offsetOfB

  static size_t offsetOfB() {
    return __builtin_offsetof (C, b);
  }
; offsetof_ref.s with g++ 4.9.2, assembly for C::offsetOfB

_ZN1C9offsetOfBEv:
.LFB0:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        movl    $4, %eax
        popl    %ebp
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
        .cfi_endproc

どちらも __builtin_offsetof という組み込み関数を使用していて、それが参照型の場合にも上手くやってくれているようです。

Both of them use the builtin function __builtin_offsetof, and it seems to handle the reference types as well.

2016年3月16日

RegExp.multiline スイッチが消えます

この記事は全体として、まもなく消えるスーパー非推奨な機能の挙動について扱っています。コードを書く時の参考にしたりしないでください。

SpiderMonkey に JavaScript 1.2 の頃からある機能に RegExp.multilineRegExp['$*'] っていうのがあります。 これはどちらも同じもので、ES6 でいうところの m フラグ相当のもの (^$ を行頭と行末にマッチさせるかどうか) を、 グローバルな状態として切り替え可能にしておこう、っていう機能です。

2016-03-16 の時点での挙動としては、新規の生成する RegExp オブジェクトの m フラグに反映されるようになってます。

# 初期状態は false です

js> RegExp.multiline
false
js> RegExp['$*']
false
js> /a/
/a/

# true にするとこれから作成する RegExp インスタンスに m フラグが付きます

js> RegExp.multiline = true
true
js> RegExp['$*']
true
js> /a/
/a/m

# RegExp コンストラクタも同様です

js> new RegExp("a")
/a/m

# false に戻すと m フラグは付かなくなります

js> RegExp.multiline = false
false
js> /a/
/a/

# 先に定義しておいた関数内にも効きます

js> function f() { return /a/; }
js> f()
/a/
js> RegExp.multiline = true
true
js> f()
/a/m

そんな RegExp.multiline ですが、現在では非標準な上に内部での扱いがとてつもなくめんどくさいので、bug 1219757 で削除されます。おそらく Firefox 48 のうちに FIXED になるでしょう。

一方 m フラグは、少し遅れて JavaScript 1.5 で追加され、標準化されました。なので、もし bug 1219757 が FIXED になった後でアドオンなんかが動かなくなった、という場合には RegExp.multilineRegExp['$*'] を使用しているコードを探し出して、m フラグを使うように書きかえてください。

2015年11月25日

RegExp.$1 と String.prototype.replace の悲惨な関係 (2)

Firefox は String.prototype.replace で関数を実行後に RegExp.$1 などを復元する、と前回の記事に書きましたが、この挙動は bug 1226936 で削除されました。

Firefox 45 からは、Chrome や Safari と同じく関数実行前に値を格納するだけ、となります。

                                    //  Firefox 45 | Chrome | Safari | Edge | IE
/(a)/.exec("a");                    // -----------+--------+--------+------+----
console.log(RegExp.$1);             //      a     |    a   |    a   |   a  |  a
"b".replace(/(b)/, function() {     //            |        |        |      |
  try {                             //            |        |        |      |
    console.log(RegExp.$1);         //      b     |    b   |    b   |   a  |  a
    "c".replace(/(c)/, function() { //            |        |        |      |
      console.log(RegExp.$1);       //      c     |    c   |    c   |   a  |  a
      /(d)/.exec("d");              //            |        |        |      |
      console.log(RegExp.$1);       //      d     |    d   |    d   |   d  |  d
      throw 1;                      //            |        |        |      |
    });                             //            |        |        |      |
  } catch (e) {}                    //            |        |        |      |
  console.log(RegExp.$1);           //      d     |    d   |    d   |   d  |  d
});                                 //            |        |        |      |
console.log(RegExp.$1);             //      d     |    d   |    d   |   b  |  b

ES6 対応もやりやすくなり、ブラウザ間の差異も軽減されていい事づくめですね。めでたしめでたし。

2015年9月7日

RegExp.$1 と String.prototype.replace の悲惨な関係



この記事は全体として非推奨の機能の挙動について扱っています。コードを書く時の参考にしたりしないでください。

この記事は Firefox 44 までの挙動をもとに書いています。詳しくは 続報の記事 を参照してください。

RegExp非推奨プロパティRegExp.$1 なんてのがありますが、これが String.prototype.replace と大変相性がよろしくないというお話です。

そもそもこの RegExp.$1 というのは仕様が存在しないにも関わらずおおよそ全てのブラウザで実装されているというもので、この時点で互換性において悲惨な予感しかしないわけです。

基本的な動作としては、最後に行った RegExp.prototype.exec もしくは同等の操作の結果をグローバルな RegExp のプロパティに保存し、これを参照できるというものです。

/(a)/.exec("a");
console.log(RegExp.$1); // a

これは内部的に RegExp.prototype.exec を呼び出す String.prototype.matchString.prototype.searchString.prototype.replace でも同様です。

"a".replace(/(a)/, "b");
console.log(RegExp.$1); // a

ここで問題になるのが、String.prototype.replace は第二引数 replaceValue として関数が渡せるということです。最初の観測結果としては、String.prototype.replace を呼び出したで、その結果が取り出せましたが、この関数の中ではどうでしょう。

"a".replace(/(a)/, function() {
  console.log(RegExp.$1);
});

結果は以下のように、ブラウザによって異なっています。

Firefox
40.0.3
Chrome
45.0.2454.85
Safari
8.0.8
Edge
20.10240.16384.0
IE
11.0.10240.16384
a a a "" ""

Edge と IE では関数を呼び出す段階では結果が入っていません。あくまでも String.prototype.replace の後で結果を取り出すもの、という扱いのようです。

今度は、この関数の中でさらに RegExp.prototype.exec を呼び出して、String.prototype.replace の後の状態を見てみましょう。

"a".replace(/(a)/, function() {
  /b/.exec("b");
});
console.log(RegExp.$1);

結果は以下のようになりました。

Firefox Chrome Safari Edge IE
a b b a a

Chrome と Safari では関数内で実行した RegExp.prototype.exec の結果のままになっています。つまり、こちらはあくまでも最後に行ったマッチングの結果という扱いのようです。ここまでの動作をまとめると以下のようになります。

  • Firefox: 関数を呼び出す前に格納、呼び出した後に復元
  • Chrome と Safari: 関数を呼び出す前に格納
  • Edge と IE: 関数を呼び出した後で結果を格納

では、さきほどのコードで RegExp.prototype.exec を実行する代わりに例外を投げてみましょう。

try {
  "a".replace(/(a)/, function() {
    throw 1;
  });
} catch (e) {
}
console.log(RegExp.$1);

結果はこうでした。

Firefox Chrome Safari Edge IE
a a a "" ""

Edge と IE では、関数が例外を投げた場合、結果は格納されません。

まとめると以下のような挙動になります。

  • Firefox: 関数を呼び出す前に格納、呼び出した後に、関数の実行結果によらず復元
  • Chrome と Safari: 関数を呼び出す前に格納
  • Edge と IE: 関数を呼び出した後で、関数が正常に終了すれば結果を格納

いろいろ組み合わせて遊んでみましょう。

                                    //  Firefox | Chrome | Safari | Edge | IE
/(a)/.exec("a");                    // ---------+--------+--------+------+----
console.log(RegExp.$1);             //     a    |    a   |    a   |   a  |  a
"b".replace(/(b)/, function() {     //          |        |        |      |
  try {                             //          |        |        |      |
    console.log(RegExp.$1);         //     b    |    b   |    b   |   a  |  a
    "c".replace(/(c)/, function() { //          |        |        |      |
      console.log(RegExp.$1);       //     c    |    c   |    c   |   a  |  a
      /(d)/.exec("d");              //          |        |        |      |
      console.log(RegExp.$1);       //     d    |    d   |    d   |   d  |  d
      throw 1;                      //          |        |        |      |
    });                             //          |        |        |      |
  } catch (e) {}                    //          |        |        |      |
  console.log(RegExp.$1);           //     c    |    d   |    d   |   d  |  d
});                                 //          |        |        |      |
console.log(RegExp.$1);             //     b    |    d   |    d   |   b  |  b

もはや全てのブラウザで同じ値を返す状況の方が少ないです。

この RegExp.$1、もちろん ES6 にも入っていませんが、代わりに String.prototype.replace の挙動の方にかなりの変更が入っています。具体的には、第一引数 searchValue@@replace プロパティによって挙動を変更できます。更に、RegExpExec の中で明示的に exec プロパティを呼ぶという風に定義されているので*1exec プロパティを変更すると RegExp を渡したにも関わらず RegExp.$1 の値に変化が無い、という状況が様々に発生します。

var re = /foo/;
re[Symbol.replace] = function() {
  return "hello";
};
console.log("abc".replace(re, "X"));  // hello

var re2 = /foo/;
re2.exec = function() {
  return { 0: "a", index: 1, length: 1 };
};
console.log("abc".replace(re2, "X")); // aXc

ここで問題になるのが Firefox と Edge、IE の挙動です。このような状況で、RegExp.$1 はどう変化すべきでしょうか。RegExp.prototype.exec も何も実行されていない場合には関数を呼んだ後に格納するものがありません。おそらく RegExp.prototype.exec が実行されたかどうか、もしくは実際に searchValue[@@replace] を実行した後の RegExp.$1 の状況によって挙動を変える、という事になるでしょうか。エンジン間の差がより大きくなりそうです。

まとめ:RegExp.$1 とかはゼッタイに使わないでね


*1 ES5 の時点では "Do the search in the same manner as in String.prototype.match, including the update of searchValue.lastIndex." として、挙動は実装まかせでした。