Hatena::ブログ(Diary)

お前の血は何色だ!! 4 このページをアンテナに追加 RSSフィード Twitter

2012-01-04

引越ししたので未来なお部屋を作ってみた。

f:id:rti7743:20120104170815j:image

年の瀬に引越ししたので、部屋の再構築を行った。

コンセプトは快適に引き篭もれる未来の部屋。

その紹介をしたいと思う。


図面

f:id:rti7743:20120104165838j:image

一部省略の図面、古い建物なのでちょっと広い。

10m2 * 2部屋ぐらい。


f:id:rti7743:20120104165906j:image

何度か検討を繰り返してこの形式に落ち着いた。



カーテンを100インチのディスプレイにしてみた

f:id:rti7743:20120104181942j:image

カーテンレールに100インチのスクリーンを釣り下げてみた。

カーテンが支えられるんだから、100インチのスクリーンぐらい支えられるだろうという発想。

f:id:rti7743:20111231224757j:image

カーテンレールに入れるのにLANケーブル用のマーカータイを使ってみた。

束ねれば結構強固である。


f:id:rti7743:20120104172434j:image

スクリーンの後ろは黒塗りされているので裏写りはないと思うけど、念のための裏写り防止のためと、昼間でも投影できるように暗室を作りたかったので、暗室用のブラックカーテンを後ろに引いている。


プロジェクターNECの NP-U310WJD 短焦点プロジェクターを使ってる。

近距離で100インチoverの画面を出せるのが魅力。

スクリーンの前にある白いのがプロジェクタ。30cmぐらいしか離れていない。


f:id:rti7743:20120104170815j:image

部屋を暗くすると、迫力ある映像が楽しめる。



D

windows media player の視覚エフェクトも100インチの画面で投影すると、迫力があってかっこいい感じになる。


音声認識

自宅のほとんどの家電は自作の音声認識システムで連動している。

音声で命令するだけで、電気のON/OFF エアコンの管理、コタツの電源入れまで何でもできる。


f:id:rti7743:20120104182236j:image

リモコンを手元においておかないで済むので、リモコン何処行った問題から解放される。

また、声で言うだけでよいので非常に楽だ。

電気を付けたい時だと、暗闇でリモコンやスイッチを探すといった苦行から解放される。

空調の管理からプロジェクタの設定まで、大抵のことはベッドの上で横になりながらでも出来てしまうのが最大の特徴だ。


f:id:rti7743:20120104171119j:image

声を拾うために集音用のマイクを2つ設置している。


認識は学習リモコンのi-remoconを自作の音声認識プログラムで制御して行なっている。

音声認識プログラムgithubで公開しているので興味があれば是非。

https://github.com/rti7743/kaden_voice


進化した音声認識は魔法と区別がつかない。

D

特技:イオナズン


コタツ

f:id:rti7743:20120104171156j:image

冬といったらコタツ

このコタツは電源を USB制御の電源タップ(ヤフオクで3000円だったよ)で制御している。

また、コタツに入ると、100インチのディスプレイの前に来れるので、またーり鑑賞モードには最適。

USBキーボード+マウスも置いているが、ノートPCも設置しているので、リモートデスクトップすることも可能である。



f:id:rti7743:20111231224735j:image

160cmの長机を利用している。

フルHDの液晶2つ以上ならべても余裕があるのが素敵だ。


キーボードFILCO キーボード

メカニカルでカチャカチャ音を立てるキーボードだけどそこそこ打ちやすくて、気に入っている。

かな文字なのは、「かな入力派」だからw

液晶は、メインが三菱の液晶

サブが BENQ の液晶

同じものにしたかったんだけど、三菱の液晶高い・・・。


ベッド

f:id:rti7743:20120104160504j:image

ベッドには液晶をアームで吊るしている。

もう8年ぐらい使っていて信頼のある構成だ。


液晶には、VESAマウント(アームが取り付けられる)液晶でフルHDで最も軽かった エイサーのディスプレイを使っている。

軽い液晶だと、VESAマウント出来なかったりするので、ちゃんと調べて買わないと泣く(泣いた)。

最初に吊るしたのが、17インチのEIZOのディスプレイでそこから数年で解像度は倍近くになり、重さは半分になった感じだ。


アームは8年前に買ったのでよく覚えてはいないが、、、アルファテック社のものを使っている。

液晶を下向きにできるアームなのが特徴。

当時はアームが殆ど無かったが、いまだといろんなアームがあるので検討してみればいいのではなイカ。


f:id:rti7743:20120104171502j:image

また、低反発枕には、スピーカーを入れていて寝ながら音声が楽しめるようにしている。


マウスには、ベッドの上でも使えるワイアレスマウスを使っている。

ベッドの上でかってに転がってポインタが進まないように、手で持たないとポインタがすすくないようにはなっている。

ただ、マウスホイールが緩くて微妙なのと裏蓋が取れやすいのが問題点。

また、ベッドからの落下で電池が外れて転がったりと以外と弱い。

ベッド上コンピューティングというジャンルは発展途上だと思うので今後に期待。


f:id:rti7743:20111231223846j:image

ベッドからそのまま100インチの画面を眺めることもできる。


照明や空調の管理は先程の音声認識で行なっているため、ほとんどをベッドの上で行うことができる。

そこで問題になるのが、ひたすら寝たきりの生活になって腰痛を患ってしまうことだww

これに対しては、ちょっと高いけど、テンピュールのマットレスを導入してカバーしている。

オフ会でテンピュールのことを教えてもらったのだが、値段は高いがかなり快適だ。


天井にプロジェクターではダメなのか?

この話をすると天井にプロジェクター投影すればいいのでは?という話しをされるがこれは良くない。

天井は中央に設置されている電気の関係で投影面積はかなり狭いのだ。

さらに、ベッドから天井までの距離を考えると、かなり大きめに投影しなければブラウザの文字を読むことはできなくなってしまうだろう。

さらに言えば、プロジェクターは天井に向かって投影できるようには設計されていないので、いろいろ工夫をすることになると思われる。

そーゆーことを考えると、やはりアームで液晶を吊るすという構成が最も適していると思われる。


クローンディスプレイ

f:id:rti7743:20120104171650j:image

f:id:rti7743:20120104171533j:image

机の上のディスプレイ2つはベッドの上のディスプレイとプロジェクタへとクローンディスプレイを行なっている。

PCにはグラフィックボード2枚刺しで、それぞれにクローンの設定をしている。


DVI分配機も検討したが、ディスプレイの電源がオフになっていると画面の表示が乱れたりしたのでボツにした。

やはり、普通にグラフィックカードを足してクローンディスプレイするのがトラブルも少なく安上がりでいいと思う。

PC

f:id:rti7743:20120104171624j:image

この環境を支えているのが隣の部屋のメタルラックにおいているPC。

青いのがメインPCだ。

うちには、メインPCとノートとストレージサーバしかない。PC何台持ってる?には1台と答えられるというとても健全な環境だ。


f:id:rti7743:20120104172850j:image

core i7の一般的なマシンに SSD 512GBをさして使っている。

512GBあるとVMを何本も入れたり、コンパイル時中間ファイルの容量なんかも気にしなくても済むので便利だ。

SSDでディスクが早いので作業も快適であり、おすすめしたい。

バックアップdropboxストレージサーバで行なっている。


f:id:rti7743:20120104173149j:image

PCには、グラフィックカード2枚さしていて、それぞれのケーブルは、机の上のディスプレイとベッドの上のディスプレイ、プロジェクタに向かっている。

USBハブに向かうケーブルが2つだけ。

USBハブは長机の足元にあるこのとベッドの下においているものになっている。

ストレージ

ストレージには、3T*6台のハードディスクを積んだ ubuntuマシンを使っている。

障害が起きた時に泣きたくないので RAID を「採用していない」。

家庭用NASで最も大切なのは、サービスの持続性ではなく、「データが消えないこと」だと考えている。

このへん詳しくは次回。


ネットワーク

f:id:rti7743:20120104172425j:image

インターネットへのアップリンクには wimax を利用している。

wimaxは契約に寄って2台まで機器を切り替えながら利用できるため、家でwimaxルータ、外に出てもノートPCのwimaxで料金そのままで通信できる。

AtermWM3500R wimaxルータだとクレードルをさせば イーサケーブルを刺すことができるので、有線LANwimaxを接合するのに役立つ。


f:id:rti7743:20120104172403j:image

wimaxは室内だと速度が出ないため、窓際、カーテンの後ろに設置している。

速度はニコニコ動画が普通に見れる程度の速度が出ているためこれで満足している。


wimaxルータ自体がwifiも提供してくれるため自動的に自宅はhotspot化している。

ただ、wifi経由だと大規模なデータを送受信できないため、PC間は 1Gイーサで繋いでいる。


なのは映画 1stは名作なのでみんなでみましょう


まとめ

引越しに伴い部屋を再構築した内容を書いた。

合理的なテクノロジーを満載した部屋ができたと思っている。

特に音声認識のおかげでリモコンの存在を忘れられるようになったのが嬉しい。

現在、SFにでてくるような、管理コンピュータシステムの開発を行なっていって、コンピュータを意識しないコンピュータシステムみたいな、そこにいるのが当たり前で生活に溶け込んだ、空気のようなコンピュータを目指していきたいと思う。



おしまい。

2011-12-26

hiphop php でPHPからジェネレートされたC++コードを読んでみよう。 (後編)

PHP PHP advent calendar です。

はてなダイアリーの投稿時の文字列制限により、前編と後編になってしまいました。

前編に続きhiphop phpPHPからジェネレートされたC++コードを読んでみようの後編を書きます。


forとwhile文

ループを見て行きましょう。

<?php
$for_sum = 0;
for($a = 0 ; $a <= 10 ; $a ++)
{
    $for_sum += $a;
}

$while_sum = 0;
$a = 10;
while($a)
{
   $while_sum += $a;
   $a --;
}

var_dump($for_sum,$while_sum);

php/test20.cpp

Variant pm_php$test20_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test20.php, pm_php$test20_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_for_sum ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss8818e723, "for_sum")) : g->GV(for_sum);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_while_sum ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf07d0c82, "while_sum")) : g->GV(while_sum);

  v_for_sum = 0LL;
  {
    LOOP_COUNTER(1);
    for (v_a = 0LL; (not_more(v_a, 10LL)); v_a++) {
      LOOP_COUNTER_CHECK(1);
      {
        v_for_sum += v_a;
      }
    }
  }
  v_while_sum = 0LL;
  v_a = 10LL;
  LOOP_COUNTER(2);
  {
    while (toBoolean(v_a)) {
      LOOP_COUNTER_CHECK(2);
      {
        v_while_sum += v_a;
        v_a--;
      }
    }
  }
  LINE(17,(x_var_dump(2, v_for_sum, Array(array_createvi(1, toVPOD(v_while_sum))))));
  return true;
}

ふつーに forループ、 whileループに変換してくれました。

ここまで、ふつーにやってくれると、ループカウンタの v_a が Variant型なのが残念ですね。

ループカウンタが int型しか取らないことはソースを見ればわかるので、int型にして欲しかったところです。

それと、 for (v_a = 0LL; (not_more(v_a, 10LL)); v_a++) の v_a++ は intなどのスカラー値じゃないんだから、C++では ++v_a にした方が早いと思うんですけど、どうなんでしょう?


ここも、まだまだ早くなる可能性があると思っておきましょう。

特にループカウンタは、チューニングの効果は絶大なはずで、今後に期待したいところです。




さて、ソースコードの中に、LOOP_COUNTER と LOOP_COUNTER_CHECK と いう見慣れないマクロがあります。

これは何をしているのでしょうか?

runtime/base/macros.h

#ifdef INFINITE_LOOP_DETECTION
#define LOOP_COUNTER(n)
#define LOOP_COUNTER_CHECK(n)                                           \
  if ((++lc & 1023) == 0) {                                             \
    check_request_timeout_ex(fi, lc);                                   \
  }
#define LOOP_COUNTER_CHECK_INFO(n)                                      \
  if ((++lc & 1023) == 0) {                                             \
    check_request_timeout_info(info, lc);                               \
  }
#else
#define LOOP_COUNTER(n)
#define LOOP_COUNTER_CHECK(n)
#define LOOP_COUNTER_CHECK_INFO(n)
#endif

無限ループを検出する機能っぽいですね。

INFINITE_LOOP_DETECTION は、Cmakefiles でにより、 INFINITE_LOOP_DETECTION=1 となっています。

ディフォルト無限ループ検出機能が働いています。

逆に言えば、俺のコードで無限ループとかマジあり得ないんですけどって人は、これをOFFにすることにより、さらに高速に動作させることが可能なわけです。

夢が広がりますね。


foreach文

PHPの foreach文は大変便利です。

C++には foreachに該当するループ文はありません。

さて、これはどのように変換されるのでしょうか?

<?php

$arr = array(0,1,2,3,4,5,6,7,8,9);
foreach($arr as $key => $val)
{
    echo "{$key} => {$val}\n";
}
Variant pm_php$test21_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test21.php, pm_php$test21_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_arr ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss7cb08d68, "arr")) : g->GV(arr);
  Variant &v_key ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss12e90587, "key")) : g->GV(key);
  Variant &v_val ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss66e0b782, "val")) : g->GV(val);

  v_arr = s_sa50d82ba2;
  {
    LOOP_COUNTER(1);
    for (ArrayIter iter3 = v_arr.begin(null_string, true); !iter3.end(); ++iter3) {
      LOOP_COUNTER_CHECK(1);
      iter3.second(v_val);
      v_key.assignVal(iter3.first());
      {
        {
          echo(toString(v_key));
          echo(NAMSTR(s_ss8f4bca72, " => "));
          echo(toString(v_val));
          echo(NAMSTR(s_ss66d2232c, "\n"));
        }
      }
    }
  }
  return true;
}

なんか、C++ STL map 、そして iteratorチックな変換になりました。

iteratorなので ループカウンタも ++iter3 と 後置インクリメントにしていますね。ココらへんは C++ の罠というか、わかりづらいところです。

言語名がC++って名前なのに、 前置インクリメント ++C を推奨しなければいけないというが理不尽ですね。

(おっと、C++の悪口はそこまでだ。 闇の軍団に消されるぞ)


変換されたソースコードSTL mapを使ったことがある人だったら、特に不思議な点はないかと思います。

break 2

PHPでは、 break 2などで2層ループをぶち抜いたbreakができます。

たぶん、perlから受け継いだ機能なんでしょうかね。

便利なんですが、 C言語から派生した言語ではあまり見ない構文です。

(どうでもいいんですけど、アスキーのマルチゲームスクリプターって言語にも break 2;がありました。知っている人だけが懐かしむネタ。)

<?php
for($a = 0 ; $a < 10 ; $a ++ )
{
    for($b = 0 ; $b < 10 ; $b ++)
    {
        if ($b == 2)
        {
           break;
        }
        if ($a == 5)
        {
           break 2;
        }
    }
}
var_dump($a,$b);

無理な制御文を使おうとすると、登場するは・・・・

Variant pm_php$test23_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test23.php, pm_php$test23_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);

  {
    LOOP_COUNTER(1);
    for (v_a = 0LL; (less(v_a, 10LL)); v_a++) {
      LOOP_COUNTER_CHECK(1);
      {
        {
          LOOP_COUNTER(2);
          for (v_b = 0LL; (less(v_b, 10LL)); v_b++) {
            LOOP_COUNTER_CHECK(2);
            {
              if (equal(v_b, 2LL)) {
                {
                  break;
                }
              }
              if (equal(v_a, 5LL)) {
                {
                  goto break1;
                }
              }
            }
          }
        }
      }
    }
    break1:;
  }
  LINE(17,(x_var_dump(2, v_a, Array(array_createvi(1, toVPOD(v_b))))));
  return true;
}

無理な制御文を使おうとすると必ず登場する goto 先生が登場しました。

これは仕方ないですね。


余談ですが、 perl の redo; などの命令を PHPC言語に変換する時も gotoを使うという選択肢が上がったりします。

もちろん、使わないでもできはするんですが、余計な構文が増えたりして、いまいちだったりします。

gotoは自由にジャンプできる柔軟性を与えてくれますが、あまりにも強力な機能なので、gotoを弱めたループ制御文を言語がもっていることが望まれます。


switch

switch文を見ていきます。

<?php

$a = 10;
switch($a)
{
case 10:
   $b = "10";
   break;
case 2:
   $b = "2";
   break;
}

var_dump($b);
Variant pm_php$test29_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test29.php, pm_php$test29_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);

  v_a = 10LL;
  switch ((v_a).hashForIntSwitch(10LL, 0LL)) {
  case 10LL:
    {
      v_b = NAMSTR(s_ssa4ae39ed, "10");
      break;
    }
  case 2LL:
    {
      v_b = NAMSTR(s_ssebb8aab9, "2");
      break;
    }
  }
  LINE(14,(x_var_dump(1, v_b)));
  return true;
}

これも、ふつーの switch文になりました。

わざわざ hashForIntSwitchメソッドなんてswitch用のメソッドを作る所がニクイですね。



しかし・・・ PHPswitchにも文字列が使えます。

C++switchC言語から受け継いだ数値や文字などの単純なものしか使えません。

caseに文字列を使うとどうなるのでしょうか?

<?php
$a = 10;
switch($a)
{
case 10:
   $b = "10";
   break;
case "2":
   $b = "2string!";
   break;
}

var_dump($b);

case "2" が文字列になりました。

さて、、、


Variant pm_php$test30_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test30.php, pm_php$test30_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);

  v_a = 10LL;
  {
    Variant switch2 = (v_a);
    if (equal(switch2, (10LL))) goto case_2_0;
    if (equal(switch2, (NAMSTR(s_ssebb8aab9, "2")))) goto case_2_1;
    goto break1;
  }

  case_2_0:
    {
      v_b = NAMSTR(s_ssa4ae39ed, "10");
      goto break1;
    }
  case_2_1:
    {
      v_b = NAMSTR(s_ss5bb3adb6, "2string!");
      goto break1;
    }
  break1:;
  LINE(14,(x_var_dump(1, v_b)));
  return true;
}

switch文から goto を使う方法に変わりました。

なので、switch文で数字型で済むときは数字で揃えとくと、 hiphopチューニングが効くみたいですね。

あんまりswitch文使う機会がないかもしれないけど、、、

例外

熱い議論が交わされることが多い例外についてです。

こいつはどのように変換されるのでしょうか?

<?php
function myfunc($a)
{
    throw new Exception("hoe-");
}

try
{
    myfunc(1);
}
catch(Exception $e)
{
    echo "catch!";
}

void f_myfunc(CVarRef v_a) {
  FUNCTION_INJECTION_NOMEM(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_a)), );
  throw_exception(LINE(5,(p_Exception(((c_Exception*)coo_Exception())->create(NAMVAR(s_svsd792abf7, "hoe-"))))));
}
namespace hphp_impl_splitter {}
Variant ifa_myfunc(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("myfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_myfunc(arg0), null);
}
Variant i_myfunc(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_myfunc);
}
CallInfo ci_myfunc((void*)&i_myfunc, (void*)&ifa_myfunc, 1, 0, 0x0000000000000000LL);
Variant pm_php$test17_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test17.php, pm_php$test17_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_e ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss6b7ea7eb, "e")) : g->GV(e);

  try {
    LINE(10,(f_myfunc(NAMVAR(s_svib794f8ce, 1LL))));
  } catch (Object e) {
    if (e.instanceof(NAMSTR(s_ssae8717ad, "exception"))) {
      v_e = e;
      echo(NAMSTR(s_ss93e46d56, "catch!"));
    } else {
      throw;
    }
  }
  return true;
}

try{} catch{} はふつーにC++言語ネイティブなものになりました。

普段、そんなにtry-catch書かないので、これをネイティブにされてもあんまり恩恵はないんですけど、、 ネイティブ化されることでコンパイラによる最適化が期待されます。

(そもそも、利用者が catch区をがりがり書かないといけないようなライブラリは設計がおかしい。)


あえて重箱の隅をつつくなら、 catch (Object e) で catch (Object& e) と参照になっていないのはなんでだろう?ってことですか。

めったに呼ばれないcatchについてーgdgdいうのもなんてけど。。。



例外を投げる方を見て行きましょう。

throw new Exception("hoe-");
↓
throw_exception(LINE(5,(p_Exception(((c_Exception*)coo_Exception())->create(NAMVAR(s_svsd792abf7, "hoe-"))))));

throw は throw_exception 関数になりました。

runtime/base/builtin_functions.cpp

void throw_exception(CObjRef e) {
  if (!Eval::Debugger::InterruptException(e)) return;
  throw e;
}

一部を除き、ふつーに C++のthrowで例外を投げています。


で、このEval::Debugger::InterruptExceptionって何だろう。

runtime/eval/debugger/debugger.cpp

bool Debugger::InterruptException(CVarRef e) {
  if (RuntimeOption::EnableDebugger) {
    ThreadInfo *ti = ThreadInfo::s_threadInfo.getNoCheck();
    if (ti->m_top && ti->m_reqInjectionData.debugger) {
      Eval::InterruptSite site(ti->m_top, e);
      Eval::Debugger::Interrupt(ExceptionThrown, NULL, &site);
      if (site.isJumping()) {
        return false;
      }
    }
  }
  return true;
}

デバッガをつないだ時の何かなんですかね? よくわかりません。

まだ先は長いので、とりあえず進みましょう。

知っている人は教えてください。

includeするテンプレート

極小な開発を除き、PHPではHTMLテンプレートをソースの中に埋め込むのではなく、外出ししてテンプレート化するのが流行りです。

includeするテンプレートC++ にどのように変換されるのか見て行きましょう。

<?php
$a = 10;
include("test24.tpl");
test24.tpl
hello <?php echo $a ?>

今回は、C++に変換されたソースが長くなるのが嫌だったので、このような書き方をしました。

このテンプレートの実装はセキュリティホールを抱えるものになってしまうので、 実際使うときは、htmlspecialchars しましょう。

cppのコードを見る前に変換されたコードのディレクトリの中を見てみます。

test24.cpp
test24.fws.h
test24.h
test24.tpl.nophp.cpp
test24.tpl.nophp.fws.h
test24.tpl.nophp.h

なんか、test24.tpl.nophp.* って奴が増えてますね。


test24.cpp

Variant pm_php$test24_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test24.php, pm_php$test24_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  v_a = 10LL;
  LINE(3,(pm_php$test24_tpl(false, variables, g)));
  return true;
}

includeはpm_php$test24_tplという関数呼び出しという形に変わりました。

さて、テンプレートの方はどうなっているのでしょうか?

test24.tpl.nophp.cpp

Variant pm_php$test24_tpl(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test24.tpl, pm_php$test24_tpl);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  echo(NAMSTR(s_ss18b6f89c, "hello "));
  echo(toString(v_a));
  echo(NAMSTR(s_ss66d2232c, "\n"));
  return true;
}

見事に、 echoに変更されました。

なんか、javaJSPの変換結果を見ているようです。

想像通りというか、まーこうするしかないので仕方ないですね。


クラス

やっと、クラスです。長かった。

まだ起きている人はいるのでしょうか。

<?php
class myclass
{
   public $privar = 10;
   public $pubvar = 20;
   public function pubfunc($a)
   {
       return $a + $this->privar + $this->pubvar;
   }
}

$class = new myclass();
$a = $class->pubfunc(10);

var_dump($a);

変換されたソースを見ると、 cls というディレクトリが増えています。

この中にラクラスを変換したヘッダーファイルが入ります。

cls/myclass.h
php/test11.cpp
php/test11.fws.h
php/test11.h

cls/myclass.h

/* SRC: test11.php line 3 */
FORWARD_DECLARE_CLASS(myclass);
extern const ObjectStaticCallbacks cw_myclass;
class c_myclass : public ObjectData {
  public:

  // Properties
  int64 m_privar;
  int64 m_pubvar;

  // Class Map
  DECLARE_CLASS_NO_SWEEP(myclass, myclass, ObjectData)
  static const ClassPropTable os_prop_table;
  c_myclass(const ObjectStaticCallbacks *cb = &cw_myclass) : ObjectData(cb, false), m_privar(10LL), m_pubvar(20LL) {}
  public: Numeric t_pubfunc(CVarRef v_a);
  DECLARE_METHOD_INVOKE_HELPERS(pubfunc);
};
ObjectData *coo_myclass() NEVER_INLINE;

ヘッダーファイルだから、定義しか書いてないようですね。

実態の方は、PHPを変換した php/test11.cpp に一緒に書いてあります。


php/test11.cpp

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
/* SRC: test11.php line 3 */
IMPLEMENT_CLASS_NO_DEFAULT_SWEEP(myclass)
const InstanceOfInfo c_myclass::s_instanceof_table[] = {
  {0x124B454009539DD2LL,1,"myclass",&cw_myclass},
};
const int c_myclass::s_instanceof_index[] = {
  1,
  0,-1,
};
CallInfo c_myclass::ci_pubfunc((void*)&c_myclass::i_pubfunc, (void*)&c_myclass::ifa_pubfunc, 1, 4, 0x0000000000000000LL);
Variant c_myclass::i_pubfunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_pubfunc);
}
Variant c_myclass::ifa_pubfunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_pubfunc, coo_myclass);
  }
  c_myclass *self ATTRIBUTE_UNUSED (static_cast<c_myclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("myclass::pubfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->t_pubfunc(arg0));
}
const MethodCallInfoTable c_myclass::s_call_info_table[] = {
  { 0x4A7B5BE10021188ALL, 1, 7, "pubfunc", &c_myclass::ci_pubfunc },
  { 0, 1, 0, 0 }
};
const int c_myclass::s_call_info_index[] = {
  1,
  0,-1,
};
const ObjectStaticCallbacks cw_myclass = {
  (ObjectData*(*)(ObjectData*))coo_myclass,
  c_myclass::s_call_info_table,c_myclass::s_call_info_index,
  c_myclass::s_instanceof_table,c_myclass::s_instanceof_index,
  &c_myclass::s_class_name,
  &c_myclass::os_prop_table,0,0,0,0x0
};
/* SRC: test11.php line 7 */
Numeric c_myclass::t_pubfunc(CVarRef v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS(myclass, myclass::pubfunc);
  INTERCEPT_INJECTION("myclass::pubfunc", array_createvi(1, toVPOD(v_a)), r);
  return ((v_a + m_privar) + m_pubvar);
}
namespace hphp_impl_splitter {}
ObjectData *coo_myclass() {
  return NEWOBJ(c_myclass)();
}
Variant pm_php$test11_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test11.php, pm_php$test11_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_class ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssc82dbd12, "class")) : g->GV(class);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(13,0);
    const p_myclass &tmp0((p_myclass(((c_myclass*)coo_myclass()))));
    v_class = tmp0;
  }
  {
    LINE(14,0);
    MethodCallPackage mcp0;
    CVarRef obj0 = v_class;
    mcp0.methodCall((obj0), NAMSTR(s_ss758adc76, "pubfunc"), 0x4A7B5BE10021188ALL);
    const CallInfo *cit0 ATTRIBUTE_UNUSED = mcp0.ci;
    Variant tmp1(((mcp0.ci->getMeth1Args())(mcp0, 1, NAMVAR(s_svi542bad8b, 10LL))));
    v_a.assignVal(tmp1);
  }
  LINE(16,(x_var_dump(1, v_a)));
  return true;
}
namespace hphp_impl_splitter {}

// Class tables
static const int64 cpt_static_inits[] = {
  (int64)&NAMVAR(s_svid7a79683, 20LL),
  (int64)&NAMVAR(s_svi542bad8b, 10LL),
};
static const ClassPropTableEntry cpt_table_entries[] = {
  {0x1CA3E3D9527D20FALL,0,0,0,68,4,GET_PROPERTY_OFFSET(c_myclass, m_pubvar),&NAMSTR(s_ssa42bbae9, "pubvar") },
  {0x27731D65F3F36A14LL,-1,1,0,68,4,GET_PROPERTY_OFFSET(c_myclass, m_privar),&NAMSTR(s_ss3b22d7c5, "privar") },

};
static const int cpt_hash_entries[] = {
  // myclass hash
  -1,-1,0,-1,1,-1,-1,-1,
  // myclass lists
  -1,
  -1,
  -1,
};
const ClassPropTable c_myclass::os_prop_table = {
  7,1,-1,-1,-1,-1,9,0,
  cpt_hash_entries+0,0,cpt_table_entries+0,cpt_static_inits
};

ぬわー。なんかただでさえ長いソースがさらに長くなりました。

実行される所を順番に見て行きましょう。


オブジェクトの構築
$class = new myclass();
↓
  {
    LINE(13,0);
    const p_myclass &tmp0((p_myclass(((c_myclass*)coo_myclass()))));
    v_class = tmp0;
  }

newでクラスを作る部分は、 coo_myclass関数の呼び出しに変わりました。

C++ネイティブに実装されたクラス p_myclass を構築していますね。


メソッドの呼び出し
$a = $class->pubfunc(10);
↓
  {
    LINE(14,0);
    MethodCallPackage mcp0;
    CVarRef obj0 = v_class;
    mcp0.methodCall((obj0), NAMSTR(s_ss758adc76, "pubfunc"), 0x4A7B5BE10021188ALL);
    const CallInfo *cit0 ATTRIBUTE_UNUSED = mcp0.ci;
    Variant tmp1(((mcp0.ci->getMeth1Args())(mcp0, 1, NAMVAR(s_svi542bad8b, 10LL))));
    v_a.assignVal(tmp1);
  }

メソッドを呼び出すのに mcp0.methodCall というのを使っています。

さらに、メソッド名を0x4A7B5BE10021188ALLという「ハッシュ値」で与えています

これにはちょっと驚きです。


関数は、C++ネイティブ関数が呼ばれるように実装されているのに、クラスメソッドは自前で名前解決をするのでしょうか?

なんでこんな実装になっているのでしょうか?

マジックメソッドなどを使われた場合に面倒になるからでしょうか?

その時だけは特殊ルーチンで逃げてくれて普通はネイティブ対応でチューニングされたソースにして欲しかったですね。

まだまだ、早くなる余地があるとプラス思考でいきましょう。



クラスメソッド呼び出しを追いかけてみましょう。

文字列で与えたメソッド名は name メンバ変数に格納しています。backtrace用か何かでしょうか?

そして、prehash という値で、クラステーブルを検索していますね。

runtime/base/builtin_functions.cpp

HOT_FUNC
bool MethodCallPackage::methodCall(CVarRef self, CStrRef method,
                                   int64 prehash /* = -1 */) {
  isObj = true;
  ObjectData *s = self.objectForCall();
  rootObj = s;
  name = &method;
  return s->o_get_call_info(*this, prehash);
}



こうやって、名前解決したものを使って、メソッドを呼び出します。

const CallInfo *cit0 ATTRIBUTE_UNUSED = mcp0.ci;
Variant tmp1(((mcp0.ci->getMeth1Args())(mcp0, 1, NAMVAR(s_svi542bad8b, 10LL))));
v_a.assignVal(tmp1);

なんで、こーゆー風になっているのか知っている人は教えてください。

とりあえずは、将来的には、C++ネイティブなクラスを呼び出すようになって、最適化されると思っておきましょう。

hiphop phpはまだまだ早くなるのかもしれないのです。


クラスの継承

せっかくクラスなんだから、継承させたいこともあるでしょう。

継承がどのように表現されるか見て行きましょう。

test25.php

<?php
class baseclass
{
     private function prifunc($a)
     {
        return $a + 1;
     }
     protected function protfunc($a)
     {
        return $a + 1;
     }
     public function pubfunc($a)
     {
        return $a + 1;
     }

}

class myclass extends baseclass
{
   public function pubfunc($a)
   {
      return $this->protfunc( $a + 2 ); //$a + 3
   }
}

$class = new myclass();
$a = $class->pubfunc(10);

var_dump($a);

今回もディレクトリを見ていきましょう。

cls/baseclass.h
cls/myclass.h
php/test25.cpp
php/test25.fws.h
php/test25.h

クラス単位でヘッダーが作られるので、 cls/baseclass.h が増えています。


順番に見て行きましょう。

cls/baseclass.h

FORWARD_DECLARE_CLASS(baseclass);
extern const ObjectStaticCallbacks cw_baseclass;
class c_baseclass : public ObjectData {
  public:

  // Properties

  // Class Map
  DECLARE_CLASS_NO_SWEEP(baseclass, baseclass, ObjectData)
  c_baseclass(const ObjectStaticCallbacks *cb = &cw_baseclass) : ObjectData(cb, false) {}
  public: Numeric t_prifunc(CVarRef v_a);
  public: Numeric t_protfunc(CVarRef v_a);
  public: virtual Variant t_pubfunc(Variant v_a);
  DECLARE_METHOD_INVOKE_HELPERS(prifunc);
  DECLARE_METHOD_INVOKE_HELPERS(protfunc);
  DECLARE_METHOD_INVOKE_HELPERS(pubfunc);
};
ObjectData *coo_baseclass() NEVER_INLINE;

おや、 private や protected で指定したものがすべて public になっていますね。

private function prifunc($a)
{
return $a + 1;
}
protected function protfunc($a)
{
return $a + 1;
}
public function pubfunc($a)
{
return $a + 1;
}

↓↓↓↓↓↓

public: Numeric t_prifunc(CVarRef v_a);
public: Numeric t_protfunc(CVarRef v_a);
public: virtual Variant t_pubfunc(Variant v_a);

コードジェネレートされたクラスに対して、アクセス修飾子なんてあんまり意味が無いので、仕方ないですね。

(最終的にアセンブラになったときなんて、すべては一直線のメモリ上でしかないんですし。)



今回は、 pubfunc を継承先で オーバーライドしているため、ちゃんと virtual がついて、public: virtual Variant t_pubfunc(Variant v_a); となっています。ココらへんはさすがですね。


継承先のクラスのヘッダを見てみましょう。

これはふつーです。

cls/myclass.h

FORWARD_DECLARE_CLASS(myclass);
extern const ObjectStaticCallbacks cw_myclass;
class c_myclass : public c_baseclass {
  public:

  // Properties

  // Class Map
  DECLARE_CLASS_NO_SWEEP(myclass, myclass, baseclass)
  c_myclass(const ObjectStaticCallbacks *cb = &cw_myclass) : c_baseclass(cb) {}
  public: virtual Variant t_pubfunc(Variant v_a);
  DECLARE_METHOD_INVOKE_HELPERS(pubfunc);
};
ObjectData *coo_myclass() NEVER_INLINE;

最後に、、、PHP本体の部分です。

今回は、クラスとメソッドが増えているので、行数はさらに増大しています。

Page Downキーの用意はいいですか?

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
/* SRC: test25.php line 20 */
IMPLEMENT_CLASS_NO_DEFAULT_SWEEP(myclass)
const InstanceOfInfo c_myclass::s_instanceof_table[] = {
  {0x635A0930E7852DD5LL,1,"baseclass",&cw_baseclass},
  {0x124B454009539DD2LL,1,"myclass",&cw_myclass},
};
const int c_myclass::s_instanceof_index[] = {
  3,
  -1,0,1,-1,
};
CallInfo c_myclass::ci_pubfunc((void*)&c_myclass::i_pubfunc, (void*)&c_myclass::ifa_pubfunc, 1, 4, 0x0000000000000000LL);
Variant c_myclass::i_pubfunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_pubfunc);
}
Variant c_myclass::ifa_pubfunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_pubfunc, coo_myclass);
  }
  c_myclass *self ATTRIBUTE_UNUSED (static_cast<c_myclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("myclass::pubfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->c_myclass::t_pubfunc(arg0));
}
const MethodCallInfoTable c_myclass::s_call_info_table[] = {
  { 0x4A7B5BE10021188ALL, 1, 7, "pubfunc", &c_myclass::ci_pubfunc },
  { 0, 1, 0, 0 }
};
const int c_myclass::s_call_info_index[] = {
  1,
  0,-1,
};
const ObjectStaticCallbacks cw_myclass = {
  (ObjectData*(*)(ObjectData*))coo_myclass,
  c_myclass::s_call_info_table,c_myclass::s_call_info_index,
  c_myclass::s_instanceof_table,c_myclass::s_instanceof_index,
  &c_myclass::s_class_name,
  0,0,0,&cw_baseclass,0x0
};
/* SRC: test25.php line 22 */
Variant c_myclass::t_pubfunc(Variant v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS_NOMEM(myclass, myclass::pubfunc);
  INTERCEPT_INJECTION("myclass::pubfunc", array_createvi(1, toVPOD(v_a)), r);
  return LINE(24,(t_protfunc((v_a + 2LL))));
}
namespace hphp_impl_splitter {}
/* SRC: test25.php line 3 */
IMPLEMENT_CLASS_NO_DEFAULT_SWEEP(baseclass)
const InstanceOfInfo c_baseclass::s_instanceof_table[] = {
  {0x635A0930E7852DD5LL,1,"baseclass",&cw_baseclass},
};
const int c_baseclass::s_instanceof_index[] = {
  1,
  -1,0,
};
CallInfo c_baseclass::ci_protfunc((void*)&c_baseclass::i_protfunc, (void*)&c_baseclass::ifa_protfunc, 1, 68, 0x0000000000000000LL);
CallInfo c_baseclass::ci_prifunc((void*)&c_baseclass::i_prifunc, (void*)&c_baseclass::ifa_prifunc, 1, 132, 0x0000000000000000LL);
CallInfo c_baseclass::ci_pubfunc((void*)&c_baseclass::i_pubfunc, (void*)&c_baseclass::ifa_pubfunc, 1, 4, 0x0000000000000000LL);
Variant c_baseclass::i_prifunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_prifunc);
}
Variant c_baseclass::i_protfunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_protfunc);
}
Variant c_baseclass::i_pubfunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_pubfunc);
}
Variant c_baseclass::ifa_prifunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_prifunc, coo_baseclass);
  }
  c_baseclass *self ATTRIBUTE_UNUSED (static_cast<c_baseclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("baseclass::prifunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->t_prifunc(arg0));
}
Variant c_baseclass::ifa_protfunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_protfunc, coo_baseclass);
  }
  c_baseclass *self ATTRIBUTE_UNUSED (static_cast<c_baseclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("baseclass::protfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->t_protfunc(arg0));
}
Variant c_baseclass::ifa_pubfunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_pubfunc, coo_baseclass);
  }
  c_baseclass *self ATTRIBUTE_UNUSED (static_cast<c_baseclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("baseclass::pubfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->c_baseclass::t_pubfunc(arg0));
}
const MethodCallInfoTable c_baseclass::s_call_info_table[] = {
  { 0x4A7B5BE10021188ALL, 1, 7, "pubfunc", &c_baseclass::ci_pubfunc },
  { 0x5620E6422EA47907LL, 1, 7, "prifunc", &c_baseclass::ci_prifunc },
  { 0x1694CB36FB987947LL, 0, 8, "protfunc", &c_baseclass::ci_protfunc },
  { 0, 1, 0, 0 }
};
const int c_baseclass::s_call_info_index[] = {
  7,
  -1,-1,0,-1,-1,-1,-1,1,

};
const ObjectStaticCallbacks cw_baseclass = {
  (ObjectData*(*)(ObjectData*))coo_baseclass,
  c_baseclass::s_call_info_table,c_baseclass::s_call_info_index,
  c_baseclass::s_instanceof_table,c_baseclass::s_instanceof_index,
  &c_baseclass::s_class_name,
  0,0,0,0,0x0
};
/* SRC: test25.php line 5 */
Numeric c_baseclass::t_prifunc(CVarRef v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS(baseclass, baseclass::prifunc);
  INTERCEPT_INJECTION("baseclass::prifunc", array_createvi(1, toVPOD(v_a)), r);
  return (v_a + 1LL);
}
namespace hphp_impl_splitter {}
/* SRC: test25.php line 9 */
Numeric c_baseclass::t_protfunc(CVarRef v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS(baseclass, baseclass::protfunc);
  INTERCEPT_INJECTION("baseclass::protfunc", array_createvi(1, toVPOD(v_a)), r);
  return (v_a + 1LL);
}
namespace hphp_impl_splitter {}
/* SRC: test25.php line 13 */
Variant c_baseclass::t_pubfunc(Variant v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS(baseclass, baseclass::pubfunc);
  INTERCEPT_INJECTION("baseclass::pubfunc", array_createvi(1, toVPOD(v_a)), r);
  return (v_a + 1LL);
}
namespace hphp_impl_splitter {}
ObjectData *coo_myclass() {
  return NEWOBJ(c_myclass)();
}
ObjectData *coo_baseclass() {
  return NEWOBJ(c_baseclass)();
}
Variant pm_php$test25_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test25.php, pm_php$test25_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_class ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssc82dbd12, "class")) : g->GV(class);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(28,0);
    const p_myclass &tmp0((p_myclass(((c_myclass*)coo_myclass())->create())));
    v_class = tmp0;
  }
  {
    LINE(29,0);
    MethodCallPackage mcp0;
    CVarRef obj0 = v_class;
    mcp0.methodCall((obj0), NAMSTR(s_ss758adc76, "pubfunc"), 0x4A7B5BE10021188ALL);
    const CallInfo *cit0 ATTRIBUTE_UNUSED = mcp0.ci;
    Variant tmp1(((mcp0.ci->getMeth1Args())(mcp0, 1, NAMVAR(s_svi542bad8b, 10LL))));
    v_a.assignVal(tmp1);
  }
  LINE(31,(x_var_dump(1, v_a)));
  return true;
}

とてつもなく長いです。

結局のところ、前回と同じように、名前解決を自前でやって、飛ばしています。

真面目に読んでいると眠くなるので次いってみよう。


inferface

邪悪ではない継承の inferface 継承を見て行きましょう。


test26.php

<?php
interface baseinterface
{
     public function pubfunc($a);
}

class myclass implements baseinterface
{
   public function pubfunc($a)
   {
      return $a + 3;
   }
}

$class = new myclass();
$a = $class->pubfunc(10);

var_dump($a);

今回もディレクトリを見ていきましょう。

cls/baseinterface.h
cls/myclass.h
php/test26.cpp
php/test26.fws.h
php/test26.h

インターフェースが定義されたbaseinterfaceを見てみます。

cls/baseinterface.h

FORWARD_DECLARE_GENERIC_INTERFACE(baseinterface);
class c_baseinterface {
  // public: virtual void t_pubfunc(CVarRef v_a) = 0;
};

おや、空っぽです。

コメントアウトされている構文の通り、 C++ でも virtual hoge() = 0; で interface が表現できるのですが、何もありません。

これは、PHPオブジェクト型への dynamic_cast がないからだと推測します。

C++で、 interface継承する理由の一つに、 dynamic_castで引き回すというのがあると思いますが、PHPではこれに該当する機能はないはずです。(たぶん)

それがない PHPC++ に変換しているため、 C++ で virtual hoge() = 0; を作ることが、コンパイラにクラス用のオブジェクトのジャンプテーブルを作成させることになってしまい、余計な負荷になってしまうため、こーゆー実装にしたのではないかと思います。

だから interface クラスは空っぽなのだと思います。



次に、myclass クラス本体の定義を見てみます。

cls/myclass.h

FORWARD_DECLARE_CLASS(myclass);
extern const ObjectStaticCallbacks cw_myclass;
class c_myclass : public ObjectData {
  public:

  // Properties

  // Class Map
  DECLARE_CLASS_NO_SWEEP(myclass, myclass, ObjectData)
  c_myclass(const ObjectStaticCallbacks *cb = &cw_myclass) : ObjectData(cb, false) {}
  public: Numeric t_pubfunc(CVarRef v_a);
  DECLARE_METHOD_INVOKE_HELPERS(pubfunc);
};
ObjectData *coo_myclass() NEVER_INLINE;

やはり、 interface継承していませんし、 t_pubfunc は virtual でもありません。

パフォーマンスを優先するために、こーゆー風になっているのでしょう。(たぶん)


最後にPHP本体です。

やっぱり長いかー

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
/* SRC: test26.php line 8 */
IMPLEMENT_CLASS_NO_DEFAULT_SWEEP(myclass)
const InstanceOfInfo c_myclass::s_instanceof_table[] = {
  {0x5EEED81878BA7A4CLL,1,"baseinterface",(const ObjectStaticCallbacks*)2},
  {0x124B454009539DD2LL,1,"myclass",&cw_myclass},
};
const int c_myclass::s_instanceof_index[] = {
  3,
  0,-1,1,-1,
};
CallInfo c_myclass::ci_pubfunc((void*)&c_myclass::i_pubfunc, (void*)&c_myclass::ifa_pubfunc, 1, 4, 0x0000000000000000LL);
Variant c_myclass::i_pubfunc(MethodCallPackage &mcp, CArrRef params) {
  return invoke_meth_few_handler(mcp, params, &ifa_pubfunc);
}
Variant c_myclass::ifa_pubfunc(MethodCallPackage &mcp, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(mcp.obj == 0)) {
    return ObjectData::ifa_dummy(mcp, count, INVOKE_FEW_ARGS_PASS_ARGS, ifa_pubfunc, coo_myclass);
  }
  c_myclass *self ATTRIBUTE_UNUSED (static_cast<c_myclass*>(mcp.obj));
  if (UNLIKELY(count < 1)) throw_missing_arguments("myclass::pubfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (self->t_pubfunc(arg0));
}
const MethodCallInfoTable c_myclass::s_call_info_table[] = {
  { 0x4A7B5BE10021188ALL, 1, 7, "pubfunc", &c_myclass::ci_pubfunc },
  { 0, 1, 0, 0 }
};
const int c_myclass::s_call_info_index[] = {
  1,
  0,-1,
};
const ObjectStaticCallbacks cw_myclass = {
  (ObjectData*(*)(ObjectData*))coo_myclass,
  c_myclass::s_call_info_table,c_myclass::s_call_info_index,
  c_myclass::s_instanceof_table,c_myclass::s_instanceof_index,
  &c_myclass::s_class_name,
  0,0,0,0,0x0
};
/* SRC: test26.php line 10 */
Numeric c_myclass::t_pubfunc(CVarRef v_a) {
  INSTANCE_METHOD_INJECTION_ROOTLESS(myclass, myclass::pubfunc);
  INTERCEPT_INJECTION("myclass::pubfunc", array_createvi(1, toVPOD(v_a)), r);
  return (v_a + 3LL);
}
namespace hphp_impl_splitter {}
ObjectData *coo_myclass() {
  return NEWOBJ(c_myclass)();
}
Variant pm_php$test26_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test26.php, pm_php$test26_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_class ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssc82dbd12, "class")) : g->GV(class);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(16,0);
    const p_myclass &tmp0((p_myclass(((c_myclass*)coo_myclass()))));
    v_class = tmp0;
  }
  {
    LINE(17,0);
    MethodCallPackage mcp0;
    CVarRef obj0 = v_class;
    mcp0.methodCall((obj0), NAMSTR(s_ss758adc76, "pubfunc"), 0x4A7B5BE10021188ALL);
    const CallInfo *cit0 ATTRIBUTE_UNUSED = mcp0.ci;
    Variant tmp1(((mcp0.ci->getMeth1Args())(mcp0, 1, NAMVAR(s_svi542bad8b, 10LL))));
    v_a.assignVal(tmp1);
  }
  LINE(19,(x_var_dump(1, v_a)));
  return true;
}

思いの外、長くないですね。

interface継承が空っぽのクラスになってしまったため、行数はそれほど伸びていません。

ここでやっていることも、クラス全般でやられていたように、自前でメソッド名を解決してアクセスしています。


global変数

この節を書こうとした人は途中で寝てしまいました。test35.php


staticメソッド

この節を書こうとした人は途中で寝てしまいました。test36.php


magic method

この節を書こうとした人は途中で寝てしまいました。test37.php

ZZzzzzzzz


まとめ

hiphop php がジェネレートする C++ のコードについて簡単に見てきました。

結構頑張った最適化が行われていると思います。


ここまで、hiphopはがんばってはいるんだけど、

それにしても、hiphop php を使っている人をあんまりみませんね。

私も使っていませんでしたけどw


やはり、そこまでwebサーバが負荷になるケースというのが少ないのでしょう。

それに、webサーバって結構簡単にスケールさせることか出来ます。

だから、入れるとしてもAPC程度のキャッシュで十分ということでしょうか。


また、いちいちコンパイルしなくてはいけないという点も問題です。

置いたら動くというPHPの機動力を失わせてしまっています。

ただし、デプロイツールをうまく作りこんでしまえば、どうとでもなる気もしますけどね。


インフラサイドから言えば、ApacheLighttpd の運用監視にみんな慣れてしまって、 hiphop php が作る HTTPD に馴染めなかったのかもしれません。

さらに、libevent と curl などの主要パッケージに独自パッチを当てたものを入れなければならず、保守が難しいという点もあります。


それに、、web業界はピンきりで、中にはdbのindexって何?って人がコード書いていたりする闇もあるかもしれませんけどね(ひゃっはー)

早くて安いサーバが変えるようになってしまったので、多少あほなコードがあっても、小中規模だと大した問題にならないみたいな。




だけれども、高速化には技術者の夢とロマンが詰まっています。

動的言語を静的言語に変換するというのも、面白いものです。

そして、それによって、パフォーマンスが2〜3倍も上がるわけですから、どういう変換が行われているかというのは興味深いところではないでしょうか?



メリークリスマス。おしまい。


ふろく

今回の記事を書くため調査したデータ一式.zip

http://rtilabs.net/files/2011_12_25/hphp_test.zip

2011-12-25

hiphop php でPHPからジェネレートされたC++コードを読んでみよう。

PHP advent calendar です。

クリスマス過ぎましたけど 12/26日をお送りします。(google docsに名前書き忘れていたんだよ)

前回 12/25日は、@yoya さんの「Windows で PHP を build する」でした。



今回は、facebookの人が作った PHP を C++ に変換して高速動作させるという hiphop php がジェネレートする C++ のコードを読んでみたいと思います。

C++は厳格な静的型づけの言語ですが、PHPは動的型づけの言語です。

これをどうやって、変換しているのか?という話です。


非常に長く、眠い話になりますが、寝ないで読んでいただけたら嬉しいです。

間違いなどありました、ぜひ教えてください。



hiphop php を入れよう。

そもそもhiphop php を入れるのはすごく大変です。

Scientific Linux 6.1 (64bit)でインストールしたときは以下のようにしたら入りました。

#追加リポジトリ
rpm -Uvh http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.2-2.el6.rf.x86_64.rpm
rpm -Uvh http://ftp.iij.ad.jp/pub/linux/fedora/epel/6/x86_64/epel-release-6-5.noarch.rpm

#アップデートと開発ツールのインストール
yum -y update
yum -y groupinstall "Development Tools"

#依存パッケージで yum で入るものを入れる.
yum -y --enablerepo=rpmforge,epel install git cmake libssh2 gd gd-devel mysql-server mysql-devel php php-cli php-common php-mbstring php-pear python-devel icu libicu-devel oniguruma oniguruma-devel flex memcached libmemcached bison re2c mysql mysql-devel libxml2 libxml2-devel libmcrypt libmcrypt-devel php-mcrypt openssl binutils binutils-devel libcap libcap-devel gd zlib bzip2 bzip2-libs bzip2-devel pcre pcre-devel expat expat-devel gd gd-devel openldap openldap-devel readline readline-devel libc-client libc-client-devel ncurses ncurses-devel pam pam-devel libmcrypt libmcrypt-devel wget tbb tbb-devel

# libmemcache
# OS標準が古いのでアップデートする
#
cd /usr/local/src/
wget http://download.tangent.org/libmemcached-0.43.tar.gz
tar -xvf libmemcached-0.43.tar.gz
cd libmemcached-0.43
./configure --prefix=/usr
make
make install


# boost
# OS標準が古いので新しいやつを /usr/に上書きで。。。
#
cd /usr/local/src/
wget 'http://sourceforge.net/projects/boost/files/boost/1.48.0/boost_1_48_0.tar.gz/download'
tar -xvf boost_1_48_0.tar.gz
cd boost_1_48_0
./bootstrap.sh
./b2 install --prefix=/usr


# download hiphop
# 先にダウンロードしないと patchが手に入らない
#
cd /usr/local/src/
git clone git://github.com/facebook/hiphop-php.git


# install libcurl  
# うちでは /usr/に上書きした
#
cd /usr/local/src/
wget http://curl.haxx.se/download/curl-7.20.0.tar.bz2
tar -xvf curl-7.20.0.tar.bz2
cp hiphop-php/src/third_party/libcurl.fb-changes.diff curl-7.20.0/
cd curl-7.20.0
sed -i 's/curl-old\///g' libcurl.fb-changes.diff
sed -i 's/curl-new\///g' libcurl.fb-changes.diff
patch -p0 < libcurl.fb-changes.diff
./configure --with-ssl --with-zlib --with-libidn --enable-sspi --enable-ldap --enable-ldaps --prefix=/usr
make
make install


# install libevent
# うちでは /usr/に上書きした
#
cd /usr/local/src/
wget http://monkey.org/~provos/libevent-1.4.13-stable.tar.gz
tar -xvf libevent-1.4.13-stable.tar.gz
cp hiphop-php/src/third_party/libevent-1.4.13.fb-changes.diff libevent-1.4.13-stable/
cd libevent-1.4.13-stable
sed -i 's/libevent-1.4.13-stable\///g' libevent-1.4.13.fb-changes.diff
sed -i 's/libevent-1.4.13-stable-fb\///g' libevent-1.4.13.fb-changes.diff
patch -p0 < libevent-1.4.13.fb-changes.diff
./configure --prefix=/usr
make
make install
ldconfig


# install hiphop
cd /usr/local/src/
cd hiphop-php/
export HPHP_HOME="/usr/local/src/hiphop-php"
export HPHP_LIB="/usr/local/src/hiphop-php/bin"
cmake -DCMAKE_PREFIX_PATH=/usr .
make
make install



#export で環境変数を定義している。ログアウトすると消えるので注意。
#毎回有効にしたい人は、  ~/.bashrc にでも。
#コピペ用
export HPHP_HOME="/usr/local/src/hiphop-php"
export HPHP_LIB="/usr/local/src/hiphop-php/bin"

参考:http://d.hatena.ne.jp/eth0jp/20101224/1293138303


libevent と curlパッチを当てないといけないのでインストールするのはなかなか大変です。

既存のRPMなどがあったりすると競合して涙目になってしまいます。

(ここらへんもhiphop php が普及しない理由の一つな気がする)


hiphop phpってどれくらい早くなるの?

苦労してインストールしたわけですから、どれくらい早くなるか気になるところです。

DBとかIOあるとそれに引きづられそうですし・・・

純粋な処理速度を見ていきたいですね。


せっかくなので、自作の Regexp_Assemble for PHP を使って速度を見て行きましょう。

Regexp_Assebmle は、正規表現を作ってくれるライブラリです。

perl の Regexp::AssebmlePHPに移植したものになります。


<?php
require_once("Regexp_Assemble.php");
$reg = new Regexp_Assemble();
$reg->add('お兄ちゃん');
$reg->add('お兄ちゃま');
$reg->add('あにぃ');
$reg->add('お兄様');
$reg->add('おにいたま');
$reg->add('兄上様');
$reg->add('にいさま');
$reg->add('アニキ');
$reg->add('兄くん');
$reg->add('兄君さま');
$reg->add('兄チャマ');
$reg->add('兄や');

//(?:お(?:兄(?:ちゃ[まん]|様)|にいたま)|兄(?:チャマ|君さま|くん|上様|や)|にいさま|あにぃ|アニキ)
echo $reg->as_string(); 

このように、単語を追加するとそれらに全てマッチする正規表現を自動的に作ってくれます。



正規表現を生成する処理は、多次元配列で管理されており、複雑な処理をCPUとメモリをフル回転させて処理します。

逆にdiskやネットワークへのIOなどはほぼありません。

純粋に処理系の速度を図りたい、このテストには向いているベンチマークだと思います。


以下のようなベンチマークを用意しました。

<?php
require_once("Regexp_Assemble.php");
for($i = 0 ; $i < 10000 ; ++$i ) {
    $reg = new Regexp_Assemble();
    $reg->add('神岸あかり');
    $reg->add('赤座あかり');
    $reg->add('黒座あかり');
    $str = $reg->as_string();

    $reg = new Regexp_Assemble();
    $reg->add('スティーブ・ジョブズ');
    $reg->add('スティーブ・ウォズアニック');
    $str = $reg->as_string();

    $reg = new Regexp_Assemble();
    $reg->add('お兄ちゃま');
    $reg->add('あにぃ');
    $reg->add('お兄様');
    $reg->add('おにいたま');
    $reg->add('兄上様');
    $reg->add('にいさま');
    $reg->add('アニキ');
    $reg->add('兄くん');
    $reg->add('兄君さま');
    $reg->add('兄チャマ');
    $reg->add('兄や');
    $str = $reg->as_string();
}

さて、ベンチマーク結果を見てみましょう。

time php bench_php.php などして計測した結果の real の値を見てみましょう。

単位は秒で、小さいほど早いです。


処理系速度
PHP 5.3.315.260s
PHP 5.4RC411.556s

PHP 5.4 で、PHPの速度も上がりました。

Regexp_Assemble for PHP は、その恩恵を受けて、PHP 5.4RC4 は PHP5.3.3の 1.32倍速となりました。

hiphop php を使うとどこまで早くなるのでしょうか?


処理系速度
PHP 5.3.315.260s
PHP 5.4RC411.556s
hiphop php5.022s

hiphop phpバイナリ変換したプログラムを、直接実行して測定しました。

src/hphp/hphp bench_php.php --keep-tempdir=1 --log=3
time /tmp/hphp_MdJIJ2/program --file bench_php.php

hiphop php早いですね。

PHP5.3の3倍、PHP5.4の 2倍以上の速さです。

こいつ赤くないのに3倍も早いゾ。

hiphop php によって facebookのwebサーバCPUトラフィックを約50%パーセント削減したというのに偽りはないようです。


http://developers.facebook.com/news.php?blog=1&story=358 の翻訳 http://blog.candycane.jp/archives/275

HipHop for PHP。
HipHopにより私たちはページによっては、Webサーバー上で約50パーセントのCPU使用量を削減できました。
CPUの使用量の少なさは、サーバー台数の削減につながり、それはより少ないオーバーヘッドを意味します。

しかし、どうやったら、こんなに高速に動作するのでしょうか?

何か魔法でも、、いやいや、プログラムは魔法で動いているのではなく、論理建てて書かれたソースコードの通りに動いています。

どんなソースコードだとこんなにも早くなるのでしょう?

今回は hiphop phpがどうやって、 PHPC++ に変換しているのかについてみていきたいと思います。

(ここまで前ふり)


ながい たびが はじまる

f:id:rti7743:20111225210347p:image

ジェネレートされたコードを読もう。

hiphop phpインストールすると、 hphpコマンドで PHPC++ に変換し、コンパイルして実行してくれます。

-m server オブジョンを付けると、自分がhttpdにもなって、web応答を返してくれます。


hiphop php は /tmp/ 以下に、 ディレクトリを作ってコードを吐きます。

コンパイルが終わると変換のソースなどは消されちゃうので、削除されないように --keep-tempdir=1 オプションをつけます。

また、どのテンポラリに吐かれたかわからなくなるので、 --log=3 オプションをつけます。

src/hphp/hphp test.php --keep-tempdir=1 --log=3

変換されたソースは以下のようなディレクトリ構成になっています。

名前役割
CMakeFilesメイク関係。コンパイルした中間ファイルなど。見なくていい。
phpPHPC++に変換したコードが格納される。主戦場
syshiphop php のシステム関係でPHP単位で変わるものがはいるみたい。
clsphp classを変換した結果。クラスが無い時は作られない。

これ以外にも、 hiphop php 自体がもっているランタイムがあります。

ランタイムは、hiphopインストールしたディレクトリsrc/runtime/ 以下にあります。

今回は /usr/local/src/hiphop-php/src/runtime になります。




今回は、 phpがどのように変換されるかなので、phpディレクトリの中を見て行きましょう。

ここには、PHPのファイル名と同名の cpp があります。

test.php に対応するには、 test.cpp test.h test.fws.h です。


早速 hello wolrdをやってみましょう。

以下のような hello worldを用意しました。

echo "hello world";

では、hiphop phpC++に変換します。

src/hphp/hphp test.php --keep-tempdir=1 --log=3

logオプションにより詳細な出力がされます。

running hphp...
creating temporary directory /tmp/hphp_vlzsBB ...
parsing inputs...
parsing inputs took 0'00" (5 ms) wall time
pre-optimizing...
pre-optimizing took 0'00" (0 ms) wall time
inferring types...
inferring types took 0'00" (0 ms) wall time
post-optimizing...
post-optimizing took 0'00" (0 ms) wall time
creating CPP files...
creating CPP files took 0'00" (48 ms) wall time
compiling and linking CPP files...

compiling and linking CPP files took 0'56" (56326 ms) wall time
running executable /tmp/hphp_vlzsBB/program --file test.php...
hello worldall files saved in /tmp/hphp_vlzsBB ...
running hphp took 0'56" (56802 ms) wall time



さて、ソースコードを見てみましょう。

元のPHPのコード test.php

echo "hello world";


hphp_test/php/test.h


#ifndef __GENERATED_php_test_h70588221__
#define __GENERATED_php_test_h70588221__


// Declarations

namespace HPHP {
///////////////////////////////////////////////////////////////////////////////

// Includes and Functions
Variant pm_php$test_php(bool incOnce, LVariableTable* variables, Globals *globals);

// Constants

///////////////////////////////////////////////////////////////////////////////
}

#endif // __GENERATED_php_test_h70588221__

php/test.fws.h


#ifndef __GENERATED_php_test_fws_h1c89c124__
#define __GENERATED_php_test_fws_h1c89c124__


namespace HPHP {
///////////////////////////////////////////////////////////////////////////////

// 1. Static Strings
extern StaticString s_ss3994978b;

// 2. Static Arrays

// 3. Static Variants





///////////////////////////////////////////////////////////////////////////////
}


#endif // __GENERATED_php_test_fws_h1c89c124__

hphp_test/php/test.cpp

#include <runtime/base/hphp.h>
#include <sys/literal_strings_remap.h>
#include <sys/scalar_arrays_remap.h>
#include <sys/scalar_integers_remap.h>
#include <sys/global_variables.h>
#include <sys/cpputil.h>
#include <php/test.fws.h>
#include <php/test.h>

// Dependencies
#include <runtime/ext/ext.h>
namespace hphp_impl_starter {}

namespace HPHP {
///////////////////////////////////////////////////////////////////////////////

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
Variant pm_php$test_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test.php, pm_php$test_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  echo(NAMSTR(s_ss3994978b, "hello world"));
  return true;
}
namespace hphp_impl_splitter {}

///////////////////////////////////////////////////////////////////////////////
}

hello world文字列が見えますね。

NAMVARってマクロがありますね。これは何でしょうか?

runtime/base/macros.h で以下のように定義されています。

#define NAMVAR(nam, str)    (nam)

むむ、つまり、文字列の方はデバッグに人間が読めるように貼っているだけというだけですか。

echo(NAMSTR(s_ss3994978b, "hello world"));
↓
↓NAMSTR マクロで置換
↓
echo(s_ss3994978b);

マクロ置換されて残った s_ss3994978bって何んでしょう?

これは、php/test.fws.h ヘッダファイルに定義があります。

hphp_test/php/test.fws.h

extern StaticString s_ss3994978b;

extern されていますね。実態はどこでしょう?

sys\literal_strings_0.no.cpp

#include <runtime/base/complex_types.h>
#include <sys/literal_strings_remap.h>


namespace HPHP {
///////////////////////////////////////////////////////////////////////////////

StaticString s_ss3994978b("hello world");

void init_literal_varstrings() {
  extern void sys_init_literal_varstrings();
  sys_init_literal_varstrings();
}

///////////////////////////////////////////////////////////////////////////////
}

定義がありました。

StaticString s_ss3994978b("hello world"); 

s_ss3994978bは、StaticString型ですね。

StaticString型とは、どういう定義がされているのでしょうか?


runtime/base/type_string.h

/**
 * A StaticString can be co-accessed by multiple threads, therefore they are
 * not thread local, and they have to be allocated BEFORE any thread starts,
 * so that they won't be garbage collected by MemoryManager. This is used by
 * constant strings, so they can be pre-allocated before request handling.
 */
class StaticString : public String {
public:
  static StringDataSet &TheStaticStringSet();
  static void FinishInit();
  static void ResetAll(); // only supposed to be called during program shutdown

public:
  friend class StringUtil;
  friend class LiteralStringInitializer;

  StaticString(litstr s);
  StaticString(litstr s, int length); // binary string
  StaticString(std::string s);
  StaticString(const StaticString &str);
  ~StaticString() {
    // prevent ~SmartPtr from calling decRefCount after data is released
    m_px = NULL;
  }
  StaticString& operator=(const StaticString &str);

private:
  void init(litstr s, int length);
  void insert();

  StringData m_data;
  static StringDataSet *s_stringSet;
};

extern const StaticString empty_string;

面白いのは、 std::string ではないということです。

すべて自作しないと嫌な人なのかな。

class StaticString : public String 
↓
class String : public SmartPtr<StringData> 
↓
class StringData

これでよくわかりました。

つまり、hello world のコードをもっと単純化して書くと、こんな感じになります。

echo(NAMSTR(s_ss3994978b, "hello world"));
↓
↓
↓
StaticString s_ss3994978b("hello world");
echo(s_ss3994978b);

hello world! が、わかった所で、次からPHPの主要な機能がどういうC++コードになっていくかを見ています。

これから先は、結構長くなるので、ヘッダーincludeやネームスペースなどの省略しています。

ちゃんとしたものが見たい方は、zipダウンロードして下さい。

zipには、今回の記事を書くため調査したデータ一式が入っています。

http://rtilabs.net/files/2011_12_25/hphp_test.zip

変数は?

次に変数を使ってみましょう。

test8.php

<?php
$a = 1;

$b = "hello";

$c = 2;
$c = "str";

$d = array();
$d[] = 10;
$d['ex'] = 20;
$d[] = 30;

var_dump($a,$b,$c,$d);

hphp_test8/php/test8.fws.h

// 1. Static Strings
extern StaticString s_ss27b9150a;
extern StaticString s_ssf8576bd5;
extern StaticString s_ss5c5509b0;
extern StaticString s_ssaa9bf7c4;
extern StaticString s_ssd2bea2d3;
extern StaticString s_sse6f62137;
extern StaticString s_ss8f83bd9c;

// 2. Static Arrays
extern StaticArray s_sa00000000;

// 3. Static Variants
extern const VarNR &s_svi542bad8b;
extern const VarNR &s_svid7a79683;
extern const VarNR &s_svifc87a5f7;

hphp_test8/php/test8.cpp

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
Variant pm_php$test8_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test8.php, pm_php$test8_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);
  Variant &v_c ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss5c5509b0, "c")) : g->GV(c);
  Variant &v_d ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssaa9bf7c4, "d")) : g->GV(d);

  v_a = 1LL;
  v_b = NAMSTR(s_sse6f62137, "hello");
  v_c = 2LL;
  v_c = NAMSTR(s_ss8f83bd9c, "str");
  v_d = s_sa00000000;
  v_d.append((NAMVAR(s_svi542bad8b, 10LL)));
  v_d.set(NAMSTR(s_ssd2bea2d3, "ex"), (NAMVAR(s_svid7a79683, 20LL)), true);
  v_d.append((NAMVAR(s_svifc87a5f7, 30LL)));
  LINE(14,(x_var_dump(4, v_a, Array(array_createvi(3, toVPOD(v_b), toVPOD(v_c), toVPOD(v_d))))));
  return true;
}

PHP変数は、Variant型で表現しています。

Variantといえば、型を持たないなんでも入れられる型の名称によく使われる名前です。

PHPの型を持たない変数を表現するのに適した名前だと思います。


さて、Variant型はどう実装されているのでしょうか?

runtime/base/type_variant.h

class Variant {
 public:
  friend class Array;
  friend class VariantVectorBase;

  /**
   * Variant does not formally derive from Countable, however it has a
   * _count field and implements all of the methods from Countable.
   */
  IMPLEMENT_COUNTABLE_METHODS_NO_STATIC
  
  
  中略

  /**
   * Constructors. We can't really use template<T> here, since that will make
   * Variant being able to take many other external types, messing up those
   * operator overloads.
   */
  Variant(bool    v) : _count(0), m_type(KindOfBoolean) { m_data.num = (v?1:0);}
  Variant(int     v) : _count(0), m_type(KindOfInt32  ) { m_data.num = v;}
  Variant(int64   v) : _count(0), m_type(KindOfInt64  ) { m_data.num = v;}
  Variant(uint64  v) : _count(0), m_type(KindOfInt64  ) { m_data.num = v;}
  Variant(long    v) : _count(0), m_type(KindOfInt64  ) { m_data.num = v;}
  Variant(double  v) : _count(0), m_type(KindOfDouble ) { m_data.dbl = v;}

  中略
};

_count と m_typeから、参照カウントをもっていて、何型か管理するフラグを使った Variant であるということがわかります。

ここでは、C++boost any ではなく自前で実装されています。。。


また、 template を使った Variant ではなく、昔ながらの何型かのフラグを保持するVariant で実装されています。

PHP の データ型との相性なのでしょうか? それとも、boost any のように template で作る場合 new が発生してしまうので、それを避けるためでしょうか?

知っている人がいたら教えて下さい。


コードを比較してみよう。

さて、コードが長くなってしまって見通しが悪いですね。

PHPC++を対訳して比較してみましょう。

PHP>$a = 1;
↓
C++>v_a = 1LL;


PHP>$b = "hello";
↓
C++>v_b = NAMSTR(s_sse6f62137, "hello");


PHP>$c = 2;
PHP>$c = "str";
↓
C++>v_c = 2LL;
C++>v_c = NAMSTR(s_ss8f83bd9c, "str");


PHP>$d = array();
PHP>$d[] = 10;
PHP>$d['ex'] = 20;
PHP>$d[] = 30;
↓
C++>v_d = s_sa00000000;
C++>v_d.append((NAMVAR(s_svi542bad8b, 10LL)));
C++>v_d.set(NAMSTR(s_ssd2bea2d3, "ex"), (NAMVAR(s_svid7a79683, 20LL)), true);
C++>v_d.append((NAMVAR(s_svifc87a5f7, 30LL)));

みごとにPHPのコードが Variant型を等して C++ で表現されています。

面白いですね。


変数がだいたい分かったところで、次は配列処理について見ていきます。

配列

前回のソースから配列処理の部分を見てみましょう。

PHP>$d = array();
PHP>$d[] = 10;
PHP>$d['ex'] = 20;
PHP>$d[] = 30;
↓
C++>v_d = s_sa00000000;
C++>v_d.append((NAMVAR(s_svi542bad8b, 10LL)));
C++>v_d.set(NAMSTR(s_ssd2bea2d3, "ex"), (NAMVAR(s_svid7a79683, 20LL)), true);
C++>v_d.append((NAMVAR(s_svifc87a5f7, 30LL)));

対訳で見て行きましょう。

$d[] = 10;

↓というコードは、appendメソッドの呼び出しに変わりました。↓

v_d.append((NAMVAR(s_svi542bad8b, 10LL)));

$d['ex'] = 20;

↓というコードは、setメソッドの呼び出しに変わりました。↓

v_d.set(NAMSTR(s_ssd2bea2d3, "ex"), (NAMVAR(s_svid7a79683, 20LL)), true);


ところで、最初の初期化に利用している s_sa00000000 とはなんなのでしょうか?

C++>v_d = s_sa00000000;
C++>v_d.append((NAMVAR(s_svi542bad8b, 10LL)));
C++>v_d.set(NAMSTR(s_ssd2bea2d3, "ex"), (NAMVAR(s_svid7a79683, 20LL)), true);
C++>v_d.append((NAMVAR(s_svifc87a5f7, 30LL)));

s_sa00000000 の定義ですが、php/test8.fws.h で定義されています。

extern StaticArray s_sa00000000;

extern なので、実態は別です。ただ、型はわかりました。

StaticArray型です。StaticArray型の定義を見てみましょう。


runtime/base/type_array.h

/**
 * A StaticArray can be co-accessed by multiple threads, therefore they are
 * not thread local, and they have to be allocated BEFORE any thread starts,
 * so that they won't be garbage collected by MemoryManager. This is used by
 * scalar arrays, so they can be pre-allocated before request handling.
 */
class StaticArray : public Array {
public:
  StaticArray() { }
  StaticArray(ArrayData *data) : Array(data) {
    m_px->setStatic();
    m_px->onSetStatic();
  }
  ~StaticArray() {
    // prevent ~SmartPtr from calling decRefCount after data is released
    m_px = NULL;
  }
};

変数は Variant型 がやりますし、 StaticArray型は何をするのでしょうか?

StaticArray で行われている機能のひとつで、静的に決定できる配列最適化があります。


自明な配列最適化

test5.php

<?php
$a = array();
$a[] = 0;
$a['er'] = 'x';
$a['acx'] = array('oge','hoge');
$a[] = 2;

var_dump($a);

これをC++に変換すると以下のようになりました。

hphp_test5/php/test5.cpp

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
Variant pm_php$test5_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test5.php, pm_php$test5_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  v_a = s_sa00000000;
  v_a.append((NAMVAR(s_svif01bca90, 0LL)));
  v_a.set(NAMSTR(s_ssfde820e1, "er"), (NAMVAR(s_svse59fa416, "x")), true);
  v_a.set(NAMSTR(s_ssf04a85df, "acx"), (s_sva905e1b1b), true);
  v_a.append((NAMVAR(s_svi90d5f98c, 2LL)));
  LINE(8,(x_var_dump(1, v_a)));
  return true;
}

対訳で載せてみましょう。

PHP>$a = array();
C++>v_a = s_sa00000000;

PHP>$a[] = 0;
C++>v_a.append((NAMVAR(s_svif01bca90, 0LL)));

PHP>$a['er'] = 'x';
C++>v_a.set(NAMSTR(s_ssfde820e1, "er"), (NAMVAR(s_svse59fa416, "x")), true);

PHP>$a['acx'] = array('oge','hoge');
C++>v_a.set(NAMSTR(s_ssf04a85df, "acx"), (s_sva905e1b1b), true); //あれ?

PHP>$a[] = 2;
C++>v_a.append((NAMVAR(s_svi90d5f98c, 2LL)));

array('oge','hoge'); の姿がありません。

代わりに s_sva905e1b1b という変数になっています。


PHP>$a['acx'] = array('oge','hoge');
C++>v_a.set(NAMSTR(s_ssf04a85df, "acx"), (s_sva905e1b1b), true); //あれ?

array('oge','hoge');の代わりに書いてある s_sva905e1b1b って何でしょうか?


sys\scalar_arrays.no.cpp

StaticArray s_sa905e1b1b;
VarNR s_sva905e1b1b;
StaticArray s_sa00000000;

void ScalarArrays::initializeNamed() {
  s_sa905e1b1b = sa_[1];
  s_sva905e1b1b = s_sa905e1b1b;
  s_sa00000000 = sa_[0];
}

回りくどいですが、 sa_[1] という変数の値になっています。

sa_[1]とは何でしょうか?

array('oge','hoge') を固定化したデータなのです。

しかもデータには gz 圧縮をされています。

sys\scalar_arrays_0.no.cpp

static const char sa_cdata[63] = {
  0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 
  0x4b, 0xb4, 0x32, 0xb2, 0xaa, 0xce, 0xb4, 0x32, 0xb0, 0x4e, 
  0xb4, 0x32, 0xb0, 0xaa, 0xae, 0xcd, 0xb4, 0x32, 0x04, 0xb2, 
  0xa0, 0x42, 0xc5, 0x56, 0xc6, 0x56, 0x4a, 0xf9, 0xe9, 0xa9, 
  0x4a, 0xd6, 0x20, 0xe1, 0x62, 0x2b, 0x13, 0x2b, 0xa5, 0x0c, 
  0x30, 0xb7, 0xb6, 0x16, 0x00, 0x63, 0x28, 0xf5, 0x8a, 0x37, 
  0x00, 0x00, 0x00, };

StaticArray ScalarArrays::sa_[2];

void ScalarArrays::initialize() {
  SystemScalarArrays::initialize();
  ArrayUtil::InitScalarArrays(sa_, 2, sa_cdata, 63);
  ScalarArrays::initializeNamed();
}


ArrayUtil::InitScalarArraysという関数初期化していますが、この関数の実体はこんな感じです。

runtime/base/array/array_util.cpp

void ArrayUtil::InitScalarArrays(Array arrs[], int nArrs,
                                 const char *scalarArrayData,
                                 int scalarArrayDataSize) {
  int len = scalarArrayDataSize;
  char *uncompressed = gzdecode(scalarArrayData, len);
  if (uncompressed == NULL) {
    throw Exception("Bad scalarArrayData %p", scalarArrayData);
  }
  String s = String(uncompressed, len, AttachString);
  Variant v(f_unserialize(s));
  ASSERT(v.isArray());
  Array scalarArrays =  v;
  ASSERT(scalarArrays.size() == nArrs);
  for (int i = 0; i < nArrs; i++) {
    arrs[i] = scalarArrays[i];
    arrs[i].setStatic();
  }
}

データをPHPの命令でもあるgzdecodeで解凍していると思われます。

hiphopPHPの命令を自前で再実装しているそうなので、それの使い回しですね。

初期化で1度だけデータを解凍するので、余計なCPUコストも掛かりませんし、圧縮すること実行ファイルが小さくなるメリットがあります。

(とはいっても、hiphopが生成するバイナリは巨大ですけど・・・)


変数文字列呼び出し

test13.php

<?php

$a = "b";
$b = 20;

$c = $$a;
var_dump($c);

PHPには、 $$a のように、変数の中に入っている文字列が表している変数名にアクセスすることができます。

これを行う C++ コードはどうなるでしょうか?


hphp_test13/php/test13.cpp

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
Variant pm_php$test13_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test13.php, pm_php$test13_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);
  Variant &v_c ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss5c5509b0, "c")) : g->GV(c);

  v_a = NAMSTR(s_ssf8576bd5, "b");
  v_b = 20LL;
  v_c.assignVal(variables->get(toString(v_a)));
  LINE(7,(x_var_dump(1, v_c)));
  return true;
}

variables->get というのがそれっぽいですね。

コードを対訳させてみましょう。


$c = $$a;
↓
v_c.assignVal(variables->get(toString(v_a)));

variablesは、 Variant pm_php$test13_php(bool incOnce, LVariableTable* variables, Globals *globals) にあるとおりです。

ローカル変数を管理するエリアだと思われます。

LVariableTable型は、 Arrayから派生しているクラスみたいです。

Arrayから派生させたのは便宜上だとは思います。

PHPの場合、「変数名」が重要になってくるため、$arr[変数名]=値 と、PHPのMapチックに使える Array構造が便利だったからでしょう。


LVariableTable型の定義を見てみましょう。

runtime/base/variable_table.h

/**
 * L-value variable table that can get/set a variable's value by its name. The
 * reason we have both RVariableTable and LVariableTable, instead of just this
 * one, is because LVariableTable requires all variables to be Variant type,
 * taking away type inference. If we can tell no dynamic variable was used in
 * l-value context, we don't have to use LVariableTable. Of course, ideally
 * even RVariableTable is not needed, meaning no dynamic variable is ever used.
 */
class LVariableTable : public Array {
 public:
  virtual ~LVariableTable() {}
  Variant &get(CVarRef s) { return getImpl(s.toString()); }
  Variant &get(CStrRef s) { return getImpl(s); }
  Variant &get(litstr  s) { return getImpl(s);}
  virtual Variant &getVar(CStrRef s, SuperGlobal sg) {
    ASSERT(sg == SgNormal);
    return getImpl(s);
  }

  /**
   * Code-generated sub-class may override this function by generating one
   * entry per variable.
   */
  virtual bool exists(CStrRef s) const {
    return Array::exists(s, true);
  }

  /**
   * Code-generated sub-class will implement this function by generating one
   * entry per variable.
   */
  virtual Variant &getImpl(CStrRef s);

  virtual Array getDefinedVars();
};

変数がどのように変換されるかだいたいわかりました。

次は、関数がどのように扱われていくのか見ていきます。

関数

PHP関数がどのように変換されるか見ていきます。

test12.php

<?php
function myfunc($v)
{
    return $v + 1;
}

$a = myfunc(3);
var_dump($a);

hphp_test12/php/test12.cpp

/* preface starts */
extern CallInfo ci_;
extern CallInfo ci_myfunc;
/* preface finishes */
/* SRC: test12.php line 3 */
Numeric f_myfunc(CVarRef v_v) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_v)), r);
  return (v_v + 1LL);
}
namespace hphp_impl_splitter {}
Variant ifa_myfunc(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("myfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_myfunc(arg0));
}
Variant i_myfunc(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_myfunc);
}
CallInfo ci_myfunc((void*)&i_myfunc, (void*)&ifa_myfunc, 1, 0, 0x0000000000000000LL);
Variant pm_php$test12_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test12.php, pm_php$test12_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(8,0);
    const Numeric &tmp0((f_myfunc(NAMVAR(s_svia6bfbbdd, 3LL))));
    v_a.assignVal(tmp0);
  }
  LINE(9,(x_var_dump(1, v_a)));
  return true;
}

ずいぶん長い C++ コードになりました。

myfunc と名前のつく関数が3つもあります。

f_myfunc , ifa_myfunc , i_myfunc 

ただ、直接呼ばれているのは、関数の実体が書いてある f_myfunc だけのようです。

{
   LINE(8,0);
   const Numeric &tmp0((f_myfunc(NAMVAR(s_svia6bfbbdd, 3LL))));
   v_a.assignVal(tmp0);
}

  ↓↓↓f_myfunc を呼び出す↓↓

Numeric f_myfunc(CVarRef v_v) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_v)), r);
  return (v_v + 1LL);
}
  

他は何に使っているのでしょうか? 今はわからないので、置いといて先に進みましょう。


関数の実体を見てみます。

Numeric f_myfunc(CVarRef v_v) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_v)), r);
  return (v_v + 1LL);
}

戻り値が Numeric型になっています。

これは、関数の中身を見て、戻り値に int型を返すパスしかないので最適化してくれた!!と思いますが、、、、実は、ただの、 Variant の typedef です。

runtime/base/types.h

typedef Variant Numeric;

int値が Variantを経由するなんてやや残念です。

int値が2^31を超えてしまったときに float型に変換されるとかの PHP の仕様が響いているのでしょうか?



他の型を返す場合はどうなるのでしょうか?

他の方を返す関数を定義してみて、挙動を観察してみます。

文字列のみを返す関数

文字列のみを返す場合は String型になります。

test28.php

<?php

function myfunc($x, $v = 1)
{
     return "abc!" . $x . $v;
}

$a = myfunc(5);
var_dump($a);


test28/php/test28.cpp

String f_myfunc(CVarRef v_x, CVarRef v_v //  = NAMVAR(s_svib794f8ce, 1LL)
) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(2, toVPOD(v_x), toVPOD(v_v)), r);
  return concat3(NAMSTR(s_ss34734aeb, "abc!"), toString(v_x), toString(v_v));
}

String型は、Variant ではなく、文字列を格納する専用の型です。

runtime/base/type_string.h

class String : public SmartPtr<StringData> {
略
}

継承している StringData型はトップレベルのクラスです。

runtime/base/string_data.h

class StringData {
略
}

Arrayを返す場合は

<?php
function myfunc($x)
{
     return array($x);
}

$a = myfunc(5);
var_dump($a);

Array f_myfunc(CVarRef v_x) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_x)), r);
  return Array(array_createvi(1, toVPOD(v_x)));
}

Array型は、Variant ではなく、配列を格納する専用の型です。

runtime/base/type_array.h

class Array : public SmartPtr<ArrayData> {
略
}

継承している ArrayData型は以下のように定義されています。

runtime/base/array/array_data.h

class ArrayData : public Countable {
略
}

関数を返す場合

<?php
function myfunc($x)
{
     return function()
     {
         return 1;
     };
}

$a = myfunc(5);
var_dump($a);
p_Closure f_myfunc(CVarRef v_x) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_x)), r);
  return LINE(7,(p_Closure(NEWOBJ(c_Closure)(&ci_07358007151305723156_1, NULL))));
}

配列を返す場合、p_Closure という型になります。

クロージャの厳密な定義とはちょっと違ってややこしいですが、そういう名前になっています。


p_Closure型ですが、これはちょっとトリッキーな名前づけをやっているようです。

p_Closure型の大元の型が c_Closure 型です。

runtime/ext/ext_closure.h

FORWARD_DECLARE_CLASS_BUILTIN(Closure);
class c_Closure : public ExtObjectData {
略
}

このとき、FORWARD_DECLARE_CLASS_BUILTINマクロによって、 typedef SmartObject<c_Closure> p_Closure; が発行されて、p_Closure型ができるみたいです。



なんつーか、grepしにくいから、こーゆー書き方やめてほしいです。。


runtime/base/macros.h

#define FORWARD_DECLARE_CLASS(cls)              \
  class c_##cls;                                \
  typedef SmartObject<c_##cls> p_##cls;         \

#define FORWARD_DECLARE_CLASS_BUILTIN(cls)      \
  FORWARD_DECLARE_CLASS(cls)                    \
  extern ObjectData *coo_##cls();               \
  extern const ObjectStaticCallbacks cw_##cls;

文字列かNULLを帰す場合

<?php
function myfunc($x)
{
    if ($x % 2 == 0)
    {
        return $x;
    }
    return NULL;
}

$x = myfunc(5);
var_dump($x);
Variant f_myfunc(CVarRef v_x) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_x)), r);
  if (equal(modulo(toInt64(v_x), 2LL), 0LL)) {
    {
      return v_x;
    }
  }
  return null;
}

ごちゃ混ぜ

<?php
function myfunc($x)
{
    if ($x % 4 == 0)
    {
        return $x;
    }
    else if ($x % 3 == 0)
    {
        return "";
    }

    return NULL;
}

$x = myfunc(5);
var_dump($x);

Variant f_myfunc(CVarRef v_x) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_x)), r);
  if (equal(modulo(toInt64(v_x), 4LL), 0LL)) {
    {
      return v_x;
    }
  }
  else if (equal(modulo(toInt64(v_x), 3LL), 0LL)) {
    {
      return NAMSTR(s_ss00000000, "");
    }
  }
  return null;
}

いろんなバリエーションを帰す場合は Variant になります。

これは当然といえば当然ですね。


戻り値を返さない関数

最後に、戻り値を返さない関数はどのように変換されるのでしょうか?

test15.php

<?php
function say($str)
{
    echo $str;
}

say("hello world!");
void f_say(CVarRef v_str) {
  FUNCTION_INJECTION_NOMEM(say);
  INTERCEPT_INJECTION("say", array_createvi(1, toVPOD(v_str)), );
  echo(toString(v_str));
}

ちゃんと戻り値void 型になりました。

関数戻り値のまとめ

場合備考
数値型NumericVariantのtypedef
文字型String
配列Array
関数p_Closuretypedef SmartObject<c_Closure> p_Closure;
別の値と混合Variant
値を返さないvoid

将来の最適化に期待したいところです。


関数ディフォルト引数

<?php
function myfunc($x, $v = 1)
{
     return $x + $v + 1;
}

$a = myfunc(5);
var_dump($a);

PHP には、 function myfunc($x, $v = 1) のように、 ディフォルト引数がつけられますね。

これは、どうなるのでしょうか?

/* preface finishes */
/* SRC: test14.php line 3 */
Numeric f_myfunc(CVarRef v_x, CVarRef v_v //  = NAMVAR(s_svib794f8ce, 1LL)
) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(2, toVPOD(v_x), toVPOD(v_v)), r);
  return (v_x + (v_v + 1LL));
}
namespace hphp_impl_splitter {}
Variant ifa_myfunc(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("myfunc", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  CVarRef arg1(count <= 1 ? (NAMVAR(s_svib794f8ce, 1LL)) : a1);
  return (f_myfunc(arg0, arg1));
}
Variant i_myfunc(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_myfunc);
}
CallInfo ci_myfunc((void*)&i_myfunc, (void*)&ifa_myfunc, 2, 0, 0x0000000000000000LL);
Variant pm_php$test14_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test14.php, pm_php$test14_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(8,0);
    const Numeric &tmp0((f_myfunc(NAMVAR(s_svi6a15d700, 5LL))));
    v_a.assignVal(tmp0);
  }
  LINE(9,(x_var_dump(1, v_a)));
  return true;
}

前回同様、 myfunc という名前の関数が3つ作られています。


関数の実態を見てみましょう。

Numeric f_myfunc(CVarRef v_x, CVarRef v_v //  = NAMVAR(s_svib794f8ce, 1LL)
) {
  FUNCTION_INJECTION(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(2, toVPOD(v_x), toVPOD(v_v)), r);
  return (v_x + (v_v + 1LL));
}

第二引数 v_v があります。

親切にも、 CVarRef v_v // = NAMVAR(s_svib794f8ce, 1LL) と、コメントが振られています。

ヘッダーファイルの方をみるとなんてことはないC++ディフォルト引数です。

php/test14.h

// Includes and Functions
Numeric f_myfunc(CVarRef v_x, CVarRef v_v = NAMVAR(s_svib794f8ce, 1LL));
Variant pm_php$test14_php(bool incOnce, LVariableTable* variables, Globals *globals);

これは、C++の特性で、ディフォルト引数をヘッダーか、実態のどちらかにしか書けないためです。

まーいけていないところですが、歴史が長い言語だけに仕方ないのかもしれません。

関数引数の型指定

型がないPHPですが、 array型などは関数引数で型指定をすることができます。

型を指定することでC++に変換するコードがどう変わるか見てみましょう。

test16.php

<?php

function myfunc(array $arr) //arrayのみ
{
   return $arr[0];
}

$a = array();
$a[] = 'a';
$a[] = 'b';
$a[] = 'c';

$b = myfunc($a);
var_dump($b);

extern CallInfo ci_myfunc;
extern CallInfo ci_;
/* preface finishes */
/* SRC: test16.php line 3 */
Variant ft_myfunc(CArrRef v_arr) {
  FUNCTION_INJECTION_NOMEM(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_arr)), r);
  return LINE(5,(v_arr.rvalAt(0LL, AccessFlags::Error)));
}
Variant f_myfunc(CVarRef v_arr) {
  if(!v_arr.isArray()) {
    throw_unexpected_argument_type(1,"myfunc()","Array",v_arr);
    return null;
  }
  return ft_myfunc(v_arr.toCArrRef());
}
namespace hphp_impl_splitter {}
namespace hphp_impl_splitter {}
Variant ifa_myfunc(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) return throw_missing_typed_argument("myfunc", 0, 1);
  CVarRef arg0(a0);
  return (f_myfunc(arg0));
}
Variant i_myfunc(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_myfunc);
}
CallInfo ci_myfunc((void*)&i_myfunc, (void*)&ifa_myfunc, 1, 0, 0x0000000000000000LL);
Variant pm_php$test16_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test16.php, pm_php$test16_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);

  v_a = s_sa00000000;
  v_a.append((NAMVAR(s_svs27b9150a, "a")));
  v_a.append((NAMVAR(s_svsf8576bd5, "b")));
  v_a.append((NAMVAR(s_svs5c5509b0, "c")));
  {
    LINE(13,0);
    const Variant &tmp0((f_myfunc(v_a)));
    v_b.assignVal(tmp0);
  }
  LINE(14,(x_var_dump(1, v_b)));
  return true;
}

myfuncという名前の関数が3つから1つ増えて、4つになってしまいました。

注目したいのは、 今まで関数の実体を定義していた f_myfunc が ft_myfuncへ処理を投げるプロキシーになった点です。

//今までここに実態があったが
Variant f_myfunc(CVarRef v_arr) {
  if(!v_arr.isArray()) {
    throw_unexpected_argument_type(1,"myfunc()","Array",v_arr);
    return null;
  }
  return ft_myfunc(v_arr.toCArrRef());
}
//関数の実体が移動された
Variant ft_myfunc(CArrRef v_arr) {
  FUNCTION_INJECTION_NOMEM(myfunc);
  INTERCEPT_INJECTION("myfunc", array_createvi(1, toVPOD(v_arr)), r);
  return LINE(5,(v_arr.rvalAt(0LL, AccessFlags::Error)));
}

f_myfunc で function myfunc(array $arr) の型チェックが入ってしまいました。

型を指定してあげることで、余計な処理が増えてしまいました。。。

型を明確に書くと、より最適化されたソースがジェネレートされるかと思いきや、余計な処理が入って遅くなってしまうとは、皮肉なものです。

PHP動的言語であり、型チェックは実行時に行わなければいけないわけで、これは仕方ないのでしょうけども。


ただ、型を明確に書くということは、プログラムの可読性、保守性を上げることであり、わずかなルーチンが挟まるからと言って、型をかかないという選択をすることは愚かなことだと思います。

型を削除することで最適化をする暇があったら、もっと別のところを見直すべきです。

組み込み関数

PHPの各込み関数がどのようにC++になるのか見て行きましょう。

test9.php

<?php
$a = "hello";
$b = strstr($a,"he");

var_dump($a,$b);

// Dependencies
#include <runtime/ext/ext.h>

中略

/* preface starts */
extern CallInfo ci_;
/* preface finishes */
Variant pm_php$test9_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test9.php, pm_php$test9_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);
  Variant &v_b ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf8576bd5, "b")) : g->GV(b);

  v_a = NAMSTR(s_sse6f62137, "hello");
  {
    LINE(3,0);
    const Variant &tmp0((x_strstr(toString(v_a), NAMVAR(s_svs546f1cb9, "he"))));
    v_b.assignVal(tmp0);
  }
  LINE(5,(x_var_dump(2, v_a, Array(array_createvi(1, toVPOD(v_b))))));
  return true;
}

PHPの strstr は、 x_strstr という関数に変わりました。

$b = strstr($a,"he");
↓↓↓↓
const Variant &tmp0((x_strstr(toString(v_a), NAMVAR(s_svs546f1cb9, "he"))));
v_b.assignVal(tmp0);

x_strstrとは何でしょうか?

runtime/ext/profile/extprofile_string.h

inline Variant x_strstr(CStrRef haystack, CVarRef needle) {
  FUNCTION_INJECTION_BUILTIN(strstr);
  TAINT_OBSERVER(TAINT_BIT_MUTATED, TAINT_BIT_NONE);
  return f_strstr(haystack, needle);
}

さらに依存している f_strstrってなんなのよ。

runtime/ext/ext_string.cpp

Variant f_strstr(CStrRef haystack, CVarRef needle) {
  Variant ret = f_strpos(haystack, needle);
  if (same(ret, false)) {
    return false;
  }
  return haystack.substr(ret.toInt32());
}

ずいぶん回りくどい書き方ですね、、、haystack.substr って何?

なお、CStrRefは、 String型の typedef です。

runtime/base/types.h

typedef const String & CStrRef;

なので、探すべきは、String::substrです。

String::substr(haystack.substr) の実装を見てみましょう。

runtime/base/type_string.cpp

String String::substr(int start, int length /* = 0x7FFFFFFF */,
                      bool nullable /* = false */) const {
  int len = size();
  char *ret = string_substr(data(), len, start, length, nullable);
  if (ret) {
    return String(ret, len, AttachString);
  }
  return String();
}

またプロキシーですか。

string_substrって何?


runtime/base/zend/zend_string.cpp

char *string_substr(const char *s, int &len, int start, int length,
                    bool nullable) {
  ASSERT(s);
  if (string_substr_check(len, start, length)) {
    len = length;
    return string_duplicate(s + start, length);
  }
  len = 0;
  if (nullable) {
    return NULL;
  }
  return string_duplicate("", 0);
}

ルーチンながっ。

呼び出し履歴を見るとこんな感じですか?

なんかもっと大胆な最適化を期待したのですが、ちょっと残念です。

まだまだ早くなる可能性があるとプラス思考でいましょう。

x_strstr
↓
f_strstr
↓
String::substr
↓
string_substr
↓
まだ続くの?

無名関数

PHP5.3 から無名関数と束縛(キャプチャ)が入って、表現力が上がりました。

クロージャやカリー化もできるようになりました。

以前も、evalや create_functionで無理やりできていたのがより機能的になった感じですか。


さて、無名関数はどのように C++ に変換されているのでしょうか?


test10.php

<?php
$func = function($param1)
{
    return $param1 + 1;
};

$a = $func(10);
var_dump($a);

php/test10.php

/* preface starts */
extern CallInfo ci_;
extern CallInfo ci_08511066943069959013_1;
/* preface finishes */
/* SRC: test10.php line 3 */
Numeric f_08511066943069959013_1(void *extra, CVarRef v_param1) {
  FUNCTION_INJECTION(08511066943069959013_1);
  INTERCEPT_INJECTION("08511066943069959013_1", array_createvi(1, toVPOD(v_param1)), r);
  return (v_param1 + 1LL);
}
namespace hphp_impl_splitter {}

Variant ifa_08511066943069959013_1(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("08511066943069959013_1", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_08511066943069959013_1(extra, arg0));
}
Variant i_08511066943069959013_1(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_08511066943069959013_1);
}

CallInfo ci_08511066943069959013_1((void*)&i_08511066943069959013_1, (void*)&ifa_08511066943069959013_1, 1, 0, 0x0000000000000000LL);
Variant pm_php$test10_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test10.php, pm_php$test10_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_func ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_sse63d8c2d, "func")) : g->GV(func);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  {
    LINE(6,0);
    p_Closure tmp0((p_Closure(NEWOBJ(c_Closure)(&ci_08511066943069959013_1, NULL))));
    v_func = tmp0;
  }
  {
    const CallInfo *cit0;
    void *vt0;
    get_call_info_or_fail(cit0, vt0, v_func);
    LINE(8,0);
    Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 10LL)));
    v_a.assignVal(tmp1);
  }
  LINE(9,(x_var_dump(1, v_a)));
  return true;
}

無名関数は、静的な関数になっています。

そして、それを包むこむように p_Closure型のクラスがあります。

C++11から 無名関数(ラムダ式)がC++にも加わったので、hiphop の登場がもう少し遅ければ、C++11の無名関数(ラムダ式)になっていたような気もします。

(そして boost lambdaが使われなかったのは謎です。)



このあと説明するのですが、無名関数は、 今まで動作が謎だった、 ifa_* の名前を持つ関数を経由して実行されるようです。

Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 10LL)));
↓
↓
↓
Variant ifa_08511066943069959013_1(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("08511066943069959013_1", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_08511066943069959013_1(extra, arg0));
}
↓
↓
↓
Numeric f_08511066943069959013_1(void *extra, CVarRef v_param1) {
  FUNCTION_INJECTION(08511066943069959013_1);
  INTERCEPT_INJECTION("08511066943069959013_1", array_createvi(1, toVPOD(v_param1)), r);
  return (v_param1 + 1LL);
}

早速、呼び出しを順に見てみましょう。

  {
    LINE(6,0);
    p_Closure tmp0((p_Closure(NEWOBJ(c_Closure)(&ci_08511066943069959013_1, NULL))));
    v_func = tmp0;
  }

c_Closure は、 p_Closure の元になったものです。

typedef SmartObject<c_Closure> p_Closure; として定義されています。

引数で渡している ci_08511066943069959013_1 というのは何でしょうか?

これは、上の方で定義されている、CallInfo型です。

CallInfo ci_08511066943069959013_1((void*)&i_08511066943069959013_1, (void*)&ifa_08511066943069959013_1, 1, 0, 0x0000000000000000LL);

関数へのポインタを渡しているようです。



CallInfoはどのようなクラスなのでしょうか?

クラスの定義を見てみましょう。

runtime/base/builtin_functions.h

class CallInfo {
public:
  enum Flags {
    VarArgs         = 0x1,
    RefVarArgs      = 0x2,
    Method          = 0x4,
    StaticMethod    = 0x8,
    CallMagicMethod = 0x10, // Special flag for __call handler
    MixedVarArgs    = 0x20,
    Protected       = 0x40,
    Private         = 0x80
  };
  CallInfo(void *inv, void *invFa, int ac, int flags, int64 refs)
    : m_invoker(inv), m_invokerFewArgs(invFa),
      m_argCount(ac), m_flags(flags), m_refFlags(refs) {}
  void *m_invoker;
  void *m_invokerFewArgs; // remove in time

〜中略〜

  FuncInvoker getFunc() const { return (FuncInvoker)m_invoker; }
  FuncInvokerFewArgs getFuncFewArgs() const {
    return (FuncInvokerFewArgs)m_invokerFewArgs;
  }
  typedef Variant (*FuncInvoker0Args)(
    void*, int);
  typedef Variant (*FuncInvoker1Args)(
    void*, int, CVarRef);
  typedef Variant (*FuncInvoker2Args)(
    void*, int, CVarRef, CVarRef);
  typedef Variant (*FuncInvoker3Args)(
    void*, int, CVarRef, CVarRef, CVarRef);
  typedef Variant (*FuncInvoker4Args)(
    void*, int, CVarRef, CVarRef, CVarRef,
    CVarRef);

〜中略〜

  typedef Variant (*FuncInvoker10Args)(
    void*, int, CVarRef, CVarRef, CVarRef,
    CVarRef, CVarRef, CVarRef, CVarRef, CVarRef, CVarRef, CVarRef);
  FuncInvoker0Args getFunc0Args() const {
    return (FuncInvoker0Args)m_invokerFewArgs;
  }
  FuncInvoker1Args getFunc1Args() const {
    return (FuncInvoker1Args)m_invokerFewArgs;
  }
  FuncInvoker2Args getFunc2Args() const {
    return (FuncInvoker2Args)m_invokerFewArgs;
  }
  FuncInvoker3Args getFunc3Args() const {
    return (FuncInvoker3Args)m_invokerFewArgs;
  }
  FuncInvoker4Args getFunc4Args() const {
    return (FuncInvoker4Args)m_invokerFewArgs;
  }

〜中略〜

  FuncInvoker10Args getFunc10Args() const {
    return (FuncInvoker10Args)m_invokerFewArgs;
  }

〜略〜

};

どうやら、CallInfoは C++関数ポインタを経由してユーザの関数を呼び出しているみたいですね。

無名関数が呼び出される過程を詳細に見ていきます。

//無名関数で呼び出し関数をセットする
CallInfo ci_08511066943069959013_1((void*)&i_08511066943069959013_1, (void*)&ifa_08511066943069959013_1, 1, 0, 0x0000000000000000LL);
Variant pm_php$test10_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test10.php, pm_php$test10_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_func ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_sse63d8c2d, "func")) : g->GV(func);
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  //無名関数に割り当てる変数 v_func の初期化
  {
    LINE(6,0);
    p_Closure tmp0((p_Closure(NEWOBJ(c_Closure)(&ci_08511066943069959013_1, NULL))));
    v_func = tmp0;
  }
  //無名関数を実行し、結果を v_a に格納する
  {
    const CallInfo *cit0;
    void *vt0;
    get_call_info_or_fail(cit0, vt0, v_func); //cit0 に v_func を紐付ける.
    LINE(8,0);
    Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 10LL)));
    v_a.assignVal(tmp1);
  }
  //結果を var_dump して表示
  LINE(9,(x_var_dump(1, v_a)));
  return true;
}

どうも、この部分で無名関数を実行しているようですね。

CallInfo::getFunc1Args メソッド で、無名関数関数ポインタを経由して実行しています。

Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 10LL)));

上にもちょっと出てきましたが、 CallInfo::getFunc1Args メソッド は、関数ポインタを返す関数になっています。

  typedef Variant (*FuncInvoker1Args)(
    void*, int, CVarRef);
  FuncInvoker1Args getFunc1Args() const {
    return (FuncInvoker1Args)m_invokerFewArgs;
  }

実行させるところまでわかりしまた。

ただ、問題が一つあります。

それは、どのようにして、 cit0->getFunc1Args() に ifa_08511066943069959013_1 関数へのポインタを格納したかです。

無名関数の情報は v_func変数に格納されています。

今回、呼び出しに使った CallInfo* cit0 へどうやって情報を渡したのでしょうか?

鍵になるのは、 get_call_info_or_fail 関数です。

  //無名関数を実行し、結果を v_a に格納する
  {
    const CallInfo *cit0;
    void *vt0;
    get_call_info_or_fail(cit0, vt0, v_func); //両者を紐付ける.
    LINE(8,0);
    Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 10LL)));
    v_a.assignVal(tmp1);
  }



get_call_info_or_fail関数の詳細を見てみましょう。

runtime/base/builtin_functions.cpp

void get_call_info_or_fail(const CallInfo *&ci, void *&extra, CVarRef func) {
  if (UNLIKELY(!get_call_info(ci, extra, func))) {
    if (func.isObject()) {
      o_invoke_failed(
        func.objectForCall()->o_getClassName().c_str(),
        "__invoke", true);
    } else {
      throw InvalidFunctionCallException(func.toString().data());
    }
  }
}


しょっぱなでget_call_info という関数を読んでいます。

runtime/base/builtin_functions.cpp

bool get_call_info(const CallInfo *&ci, void *&extra, CVarRef func) {
  Variant::TypedValueAccessor tv_func = func.getTypedAccessor();
  if (Variant::GetAccessorType(tv_func) == KindOfObject) {
    ObjectData *d = Variant::GetObjectData(tv_func);
    ci = d->t___invokeCallInfoHelper(extra);
    return ci != NULL;
  }
  if (LIKELY(Variant::IsString(tv_func))) {
    StringData *sd = Variant::GetStringData(tv_func);
    return get_call_info(ci, extra, sd->data(), sd->hash());
  }
  return false;
}

なんか、それっぽいルーチンにきました。

第三引数 func つまり、v_func という無名関数関数情報をもっている変数を調べて、 第一引数 ci に格納しています。

こうやって、無名関数を CallInfo オブジェクトに関連付けて、実行しているのです。


しかし、結構回りくどい飛び出し方ですね。。。

C++11も出たことですし、C++11な lambdaを生成して欲しいものです。


束縛(キャプチャ)

無名関数変数を束縛(キャプチャ)することができます。

キャプチャはどのようにC++に変換されるのでしょうか?


test27.php

<?php
$cap = 10;
$func = function($a) use($cap)
{
    return $a + $cap + 1;
};

$c = $func(2); //2 + 10 + 1
var_dump($c);

php/test27.cpp

extern CallInfo ci_;
extern CallInfo ci_07296613028076329051_1;
FORWARD_DECLARE_CLASS(Closure$07296613028076329051_1);
class c_Closure$07296613028076329051_1 : public c_Closure {
  public:
  DECLARE_OBJECT_ALLOCATION_NO_SWEEP(c_Closure$07296613028076329051_1)
  Variant v_cap;

  c_Closure$07296613028076329051_1(const CallInfo *func, void *extra, CVarRef r_cap) : c_Closure(func, extra) {
    v_cap.assignVal(r_cap);
  }
};
/* preface finishes */
/* SRC: test27.php line 4 */
Numeric f_07296613028076329051_1(void *extra, CVarRef v_a) {
  FUNCTION_INJECTION(07296613028076329051_1);
  INTERCEPT_INJECTION("07296613028076329051_1", array_createvi(1, toVPOD(v_a)), r);
  c_Closure$07296613028076329051_1 *closure ATTRIBUTE_UNUSED = (c_Closure$07296613028076329051_1*)extra;
  Variant v_cap;

  v_cap.assignVal(closure->v_cap);
  return (v_a + (v_cap + 1LL));
}
namespace hphp_impl_splitter {}
Variant ifa_07296613028076329051_1(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("07296613028076329051_1", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_07296613028076329051_1(extra, arg0));
}
Variant i_07296613028076329051_1(void *extra, CArrRef params) {
  return invoke_func_few_handler(extra, params, &ifa_07296613028076329051_1);
}
CallInfo ci_07296613028076329051_1((void*)&i_07296613028076329051_1, (void*)&ifa_07296613028076329051_1, 1, 0, 0x0000000000000000LL);
IMPLEMENT_OBJECT_ALLOCATION_NO_DEFAULT_SWEEP(c_Closure$07296613028076329051_1)
Variant pm_php$test27_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test27.php, pm_php$test27_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_cap ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf0ead937, "cap")) : g->GV(cap);
  Variant &v_func ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_sse63d8c2d, "func")) : g->GV(func);
  Variant &v_c ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss5c5509b0, "c")) : g->GV(c);

  v_cap = 10LL;
  {
    LINE(7,0);
    p_Closure tmp0((p_Closure$07296613028076329051_1(NEWOBJ(c_Closure$07296613028076329051_1)(&ci_07296613028076329051_1, NULL, v_cap))));
    v_func = tmp0;
  }
  {
    const CallInfo *cit0;
    void *vt0;
    get_call_info_or_fail(cit0, vt0, v_func);
    LINE(9,0);
    Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 2LL)));
    v_c.assignVal(tmp1);
  }
  LINE(10,(x_var_dump(1, v_c)));
  return true;
}

上から眺めてみて、無名関数の時との違いを見てみましょう。


無名関数になかったクラスが生成されています。

FORWARD_DECLARE_CLASS(Closure$07296613028076329051_1);
class c_Closure$07296613028076329051_1 : public c_Closure {
  public:
  DECLARE_OBJECT_ALLOCATION_NO_SWEEP(c_Closure$07296613028076329051_1)
  Variant v_cap;

  c_Closure$07296613028076329051_1(const CallInfo *func, void *extra, CVarRef r_cap) : c_Closure(func, extra) {
    v_cap.assignVal(r_cap);
  }
};

無名関数が束縛する変数を管理するためのクラスだと思われます。

今回キャプチャするcap変数っぽい名前が見えますね。


次にルーチンを見て行きましょう。

v_cap という変数をp_Closure tmp0 の初期化時に渡しています。

これでこの変数の値を「コピーして保持」するみたいです。

Variant pm_php$test27_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test27.php, pm_php$test27_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_cap ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ssf0ead937, "cap")) : g->GV(cap);
  Variant &v_func ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_sse63d8c2d, "func")) : g->GV(func);
  Variant &v_c ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss5c5509b0, "c")) : g->GV(c);

  v_cap = 10LL;
  {
    LINE(7,0);
    p_Closure tmp0((p_Closure$07296613028076329051_1(NEWOBJ(c_Closure$07296613028076329051_1)(&ci_07296613028076329051_1, NULL, v_cap))));
    v_func = tmp0;
  }
  {
    const CallInfo *cit0;
    void *vt0;
    get_call_info_or_fail(cit0, vt0, v_func);
    LINE(9,0);
    Variant tmp1(((cit0->getFunc1Args())(vt0, 1, 2LL)));
    v_c.assignVal(tmp1);
  }
  LINE(10,(x_var_dump(1, v_c)));
  return true;
}

それ以外は、無名関数の時と同じに思えます。

get_call_info_or_fail(cit0, vt0, v_func); で、const CallInfo *cit0; に無名関数関数を関連付けて、 cit0->getFunc1Args() を経由して、呼び出します。

呼び出されると、ifa_* 系の関数である ifa_07296613028076329051_1 を経由して、無名関数本体が呼び出されます。

Variant ifa_07296613028076329051_1(void *extra, int count, INVOKE_FEW_ARGS_IMPL_ARGS) {
  if (UNLIKELY(count < 1)) throw_missing_arguments("07296613028076329051_1", count+1);
  CVarRef arg0(UNLIKELY(count <= 0) ? null_variant : a0);
  return (f_07296613028076329051_1(extra, arg0));
}


↓

Numeric f_07296613028076329051_1(void *extra, CVarRef v_a) {
  FUNCTION_INJECTION(07296613028076329051_1);
  INTERCEPT_INJECTION("07296613028076329051_1", array_createvi(1, toVPOD(v_a)), r);
  c_Closure$07296613028076329051_1 *closure ATTRIBUTE_UNUSED = (c_Closure$07296613028076329051_1*)extra;
  Variant v_cap;

  v_cap.assignVal(closure->v_cap);
  return (v_a + (v_cap + 1LL));
}

この時、 void *extra には、キャプチャしたcap変数の情報をもつ クラスのインスタンスが入ります。

それにより、キャプチャした本数に無名関数アクセスできます。


やっと、関数が終わりました。

まだ眠くないですか?

眠くないなら、次は制御文を見て行きましょう。

if文

制御文の代表格 IF文を見て行きましょう。

test18.php

<?php
$a = 10;
if ($a == 10 ){
  echo "10"; 
}
else
{
  echo "else!"; 
}

Variant pm_php$test18_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test18.php, pm_php$test18_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  v_a = 10LL;
  if (equal(v_a, 10LL)) {
    {
      echo(NAMSTR(s_ssa4ae39ed, "10"));
    }
  }
  else {
    {
      echo(NAMSTR(s_ss2dcad959, "else!"));
    }
  }
  return true;

これは、綺麗に C++のIF文になりました。

文句の言いようがありません。

PHPの == 比較演算子は equal関数呼び出しに変わりました。

PHPの == 比較演算子ってクセがあるので、単純な C++ の ==比較演算子変換できないのも無理はありません。


===演算子

PHPdisる人が毎回毎回標的にあげてきて、その人気に嫉妬したくなる === 比較演算子を見てきます。

test19.php

<?php
$a = 10;
if ($a === 10 ){
  echo "10"; 
}
else
{
  echo "else!"; 
}

Variant pm_php$test19_php(bool incOnce, LVariableTable* variables, Globals *globals) {
  PSEUDOMAIN_INJECTION(run_init::test19.php, pm_php$test19_php);
  LVariableTable *gVariables ATTRIBUTE_UNUSED = (LVariableTable *)g;
  Variant &v_a ATTRIBUTE_UNUSED = (variables != gVariables) ? variables->get(NAMSTR(s_ss27b9150a, "a")) : g->GV(a);

  v_a = 10LL;
  if (same(v_a, 10LL)) {
    {
      echo(NAMSTR(s_ssa4ae39ed, "10"));
    }
  }
  else {
    {
      echo(NAMSTR(s_ss2dcad959, "else!"));
    }
  }
  return true;
}

見ての通りです。

PHPの ===比較演算子は、same関数呼び出しに変わりました。

なんか、あっけないほど簡単ですね。


PHPhiphop php
==equal
===same

後編に続く

記事が長すぎて、プレビューでは問題ないのに、投稿したときにぶった切られて切れてしまうみたいです。

はてなダイアリーの限界に挑んでいるんでしょうか。

hiphop php でPHPからジェネレートされたC++コードを読んでみよう(後編)に続きます。

2011-12-20

なのは完売 とある関数の電脳戦 (じょうほうせん とある関数のバトルプログラム)

カーネル/VM Advent Calendar の記事です。

それは、小さなメモリチップの中で繰り広げられる電脳戦のお話。

ひと足早いなのは完売(じょうほうせん)を楽しみましょう。


f:id:rti7743:20111220224719j:image

特定の処理の中に、自分の任意の処理を挟む行為をフックと呼びますが、フックから自分のプロセスを守りぬく方法はあるのでしょうか?

windows環境でのanti hookのお話をします。


DLL Injection Hook

f:id:rti7743:20111220224832j:image

windowsではさまざまなフック形式を用いることができます。

ほとんどのフックにおいて、最終的には自分のフック用DLLを相手のプロセスに叩きこんで、そこを橋頭堡として、フックを行います。

これはOSプロセスのメモリ保護をかいくぐるのにフックDLLが便利だからです。

(ついでに8086のジャンプが相対アドレスってゆーのもありそうですがね。)


これらのフックは、DLLを対象プロセスに挿入するので、DLL INJECTIONとも呼ばれています。

フックするプロセスは、APISetWindowsHookExCreateRemoteThread によって、対象プロセスDLLマッピングを行います。


早速、サンプルを見て行きましょう

攻撃をするドSくん と 攻撃を受ける ドMくんです。


ドSくんは、ドMくんに CreateRemoteThread API を使い、ドS攻撃用DLL.dll というフック用DLLを挿入します。ねじ込んだるー。

ドS攻撃用DLL.DLLによって、ドMくんのプロセスに干渉します。


今回は、ドMくんの「なのフェイの薄い本ください」という文字列を「なのは完売」という文字列に書き換えます。

この程度であれば、フックなんて使わなくてもできるのですが、サンプルということで、お願いします。

f:id:rti7743:20111220225227j:image


実際動かしてみましょう。

ドSくんとドMくんを起動して、ドSくんのボタンをクリックすると、ドMくんのステータスが「なのフェイの薄い本ください」から「なのは完売」に変わります。


サンプルプログラム 「ドS攻撃CreateRemoteThreadとDLL_Injection」と「ドMやられ放題」による実演。

f:id:rti7743:20111220222438p:image



攻撃のキモの部分を見てみましょう。

ここで、ドSくんは、ドMくんのプロセスに、ドS攻撃用DLL.DLLをねじ込んでいます。らめー。

//CreateRemoteThread で ドM を攻撃する
xreturn::r<bool> Attach()
{
	HMODULE kernel32dll = ::GetModuleHandle("Kernel32");

	//埋め込むフック用のdll
	char injectionDLLName[MAX_PATH];
	if( !GetModuleFileName( g_Instance ,injectionDLLName,_MAX_PATH) )
	{
		DWORD lastError = ::GetLastError();
		return xreturn::windowsError(std::string() + "GetModuleFileNameに失敗。" , lastError);
	}
	_tcscpy(_tcsrchr(injectionDLLName, '\\') + 1, "ドS攻撃用DLL_CreateRemoteThread.dll" );

	//ドMくんのプロセスを探します。
	auto processID = findTargetProcess();
	if (processID.isError())
	{
		DWORD lastError = ::GetLastError();
		return xreturn::windowsError(std::string() + "GetModuleFileNameに失敗。" , lastError);
	}
	if ( (DWORD)processID == 0)
	{
		return xreturn::error(std::string() + "攻撃対象がいません" ,0);
	}

	//ドMくんのプロセスをひらきます。
	HANDLE processHandle = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ  | PROCESS_VM_WRITE , FALSE,processID );
	if (!processHandle)
	{
		DWORD lastError = ::GetLastError();
		return xreturn::windowsError(std::string() + "OpenProcessに失敗。" , lastError);
	}

	//文字列用のメモリを確保するために、ドMくん名義でメモリを確保しておきます。
	//おれおれドMなんだけどメモリをちょっと振り込んでくれないかなー?
	void * remoteMemory = VirtualAllocEx( processHandle, NULL , lstrlen(injectionDLLName) , MEM_COMMIT, PAGE_EXECUTE_READWRITE );
	if (!remoteMemory)
	{
		DWORD lastError = ::GetLastError();
		CloseHandle(processHandle);
		return xreturn::windowsError(std::string() + "VirtualAllocExに失敗。" , lastError);
	}
	::WriteProcessMemory(processHandle, remoteMemory, (void*)injectionDLLName,lstrlen(injectionDLLName),NULL);

	//ターゲットプロセス上にスレッドを作成し、dllを注入します。

	HANDLE remoteThreadHandle;
	//フック
	remoteThreadHandle = ::CreateRemoteThread( processHandle, NULL, 0,	
					(LPTHREAD_START_ROUTINE) ::GetProcAddress(kernel32dll,"LoadLibraryA"), 
					remoteMemory, 0, NULL );

	if( !remoteThreadHandle )
	{
		DWORD lastError = ::GetLastError();

		::VirtualFreeEx( processHandle, injectionDLLName, lstrlen(injectionDLLName) , MEM_RELEASE );
		CloseHandle(processHandle);

		return xreturn::windowsError(std::string() + "CreateRemoteThreadに失敗。" , lastError);
	}

	//注入スレッドが終了するまで待ちます.
	::WaitForSingleObject( remoteThreadHandle, INFINITE );

	DWORD hLibModule;
	::GetExitCodeThread( remoteThreadHandle, &hLibModule );

	::VirtualFreeEx( processHandle, injectionDLLName, lstrlen(injectionDLLName) , MEM_RELEASE );
	::CloseHandle( processHandle );
	return true;
}

フック用のDLLを対象プロセスにねじ込んだあとは、DLLの中の任意のコードを動作させることができます。

そのため、フック対象プロセス蹂躙し放題です。

(こっ、こんなDLLなんかにビクンビクン。)



たとえドMでも、よくしらない相手に好き放題されるのは悔しいものです。


これは、ドMくんでも、くやしいですね。悔しいはず無いですわよね。

悔しいので、対抗策を考えます。


anti DLL Injection

結局のところ、自分のプロセス内で LoadLibraryA を呼ばれているわけですから、それを呼ばせないようにすればいいわけです。

LoadLibrary が素直すぎて、ドSくんからの攻撃を素直に実行しているのが悪いのです。

つまり、ドSくんからフック用のDLLをねじ込まれるよりも早く、 自分のLoadLibrary API を先にフックして、ドSくんの攻撃だったらロードしない、といった、賢いLoadLibrary関数に作り替えてしまうのです。

f:id:rti7743:20111220225420j:image



API フックの方法はいろいろありますが、今回は 自作のsexyhookでやってみます

sexyhookもひっそりとバージョンアップを重ねているのだ。

sexyhookのgithubはこっち


こんな感じです。

//LoadLibraryAを保護する.
SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
			("kernel32.dll","LoadLibraryA"),( const char * lpLibFileName ))
{

	//ドSくんのDLLだけロードさせない.
	if ( strstr(lpLibFileName , "ドS" ))
	{
		//ドSって入っているから、ドSくんのdllだろうから、ロード拒否.
		MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0);

		return 1;
	}
	//それ以外はロードする.
	//全部のDLLを拒否してはいけない。
	//gdiplus や winsock など遅延読み込みされるAPIがあるため、そいつらまで殺してしまう。

	//一時的にフックを解除
	SEXYHOOK_UNHOOK();
	//元の関数を呼び出す
	return CallOrignalFunction(lpLibFileName);
}
SEXYHOOK_END_AS() g_LoadLibraryAHook;

これで、ドSくんがDLLをねじ込もうとしても、スルーできます。

これをバリアと名づけましょう。手をクロスしてバリアーーー!!

f:id:rti7743:20111220225458j:image


ドMくんのバリアーボタンをクリックすると、この処理が行われるようになっています。

試してしみましょう。

サンプルプログラム 「ドS攻撃CreateRemoteThreadとDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_LoadLibraryA」による実演

バリアーを展開すると、今度はフックされないよ。

f:id:rti7743:20111220222622p:image


見事に、ドSくんの攻撃を受け流すことが出来ました。

ステータスは「なのフェイの薄い本ください」であり、フックされて書き換えられる「なのは完売」ではありません。

やったね☆

だが、これだけでは完璧とは言えない

これで、フックを受け流すことが出来ました。


ですが、LoadLibray関数は複数種類ありますよね。

LoadLibrayA LoadLibrayW LoadLibrayExA LoadLibrayExW です。


今回は、相手がLoadLibrayAを呼ぶので、これだけ対応しましたが、ドSくん以外の攻撃者はどれを使ってくるかわかりません。


また、 SetWindowsHookEx APIによるフックは LoadLibraryのフックだけでは防げないようです。

デモを見てみましょう。


サンプルプログラム 「ドS攻撃SetHookWindowsExでDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_LoadLibraryA」による実演

f:id:rti7743:20111220222655j:image

SetHookWindowsEx API によるフックでは、 dll内に実装した SetHookWindowsEx を呼び出すことで、 windows OS から DLLが各プロセスマッピングされます。

LdrLoadDll

こうなって、全部に対応する・・・のは少々めんどいですね。

実は、LdrLoadDllという、 LoadLibray関数の元締め的関数がいるのです。

f:id:rti7743:20111220225637j:image


LdrLoadDllをフックすれば、すべての LoadLibray関数 をフックすることができます。

ついでに、 SetWindowsHookEx にも対応できちゃいます。

早速やってみましょう。

//LoadLibrary* 系すべてでバリアを使う.
SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
		("NTDLL.dll","LdrLoadDll"),(PWCHAR PathToFile,ULONG Flags,PUNICODE_STRING ModuleFileName,PHANDLE ModuleHandle))
{
	//気に食わないdllはロードさせない. 
	if (wcsstr(ModuleFileName->Buffer ,L"ドS" ) != NULL)
	{
		//ドSって入っているから、ドSくんのdllだろうから、ロード拒否.
		MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0);

		return 0; //ロード禁止
	}

	//それ以外はふつーにロードするよ

	//一時的にフックを解除
	SEXYHOOK_UNHOOK();
	//元の関数を呼び出す
	return CallOrignalFunction(PathToFile,Flags,ModuleFileName,ModuleHandle);
}
SEXYHOOK_END_AS() g_LdrLoadDllHook;

これだけです。簡単ですね。

デモで挙動を見てみましょう。

サンプルプログラム 「ドS攻撃SetHookWindowsExでDLL_Injection」と「ドMやられ役_Anti_DLL_Injection_完全版」による実演

f:id:rti7743:20111220222719p:image


やりました。

これで、どのDLLをロードして、どのDLLをロードしないかという決定権を自プロセスが握ることが出来ました。

あとは、この中で、DLLの名前によって、ホワイトリストによるフィルタリングをするにしろ、ブラックリストによるフィルタリングをするにしろ自由に作ることができます。

やったね☆。


これで、DLL Injectionによるフックからは身を守れるようになりました。

めでした。めでした・・・・・ってまだ続くよ。

余談 LdrLoadDll ですべて拒否してはいけない。

海外のフォーラムでは LdrLoadDll を書き変えて CHAR pRet[]={0xC3}; つまり ret 命令にしたりしています。


これでもいいんですが、 これでは、winsock や gdiplus といった遅延読み込みする dll も、読み込みを拒絶してしまい、プログラムがまともに動かないことがあります。


そういうDLLを使っている場合は、今回の例みたいに、DLLの名前でロードするかどうかを決める処理を入れる必要があります。


anti CreateRemoteThread

前回は、橋頭堡として DLL injectionしてくる奴だったので、LdrLoadDll で逃げれました。

ですが、DLL injectionしてこないフックだったらどうでしょうか?


CreateRemoteThread で直接アセンブラなどを叩きこんでくるフックルーチンです。


そんなことできるの?って話しですが、できます。

フック用DLLを作るのがOSのメモリ保護を出しぬいてフック用関数楽に書きたいためであって、フックにDLL必須ではありません。

だから、DLLなんかなくても当然フックできます。



サンプルプログラム 「ドS攻撃CreateRemoteThreadだけ」と「ドMやられ役_Anti_DLL_Injection_完全版」による実演。

f:id:rti7743:20111220222758p:image


DLL Injectionは完璧に守れていた 「ドMやられ役_Anti_DLL_Injection_完全版」の無敵のバリアが突破されてしまいました。

ネクロフォビア「む、無敵のバリアが。」

助けて、金ピカ。

f:id:rti7743:20111220225810j:image


さて、どうしましょう。

CreateRemoteThreadをフックして・・・・というのは無意味です。

プロセスのCreateRemoteThread関数が呼ばれるわけではないですからね。


だったら、全プロセスのCreateRemoteThreadを先にフックしておいて・・・・って考えるのはナンセンスです。

それって、自分へのフック攻撃を避けるために、相手をフックするわけですよね。


やはり、ここは専守防衛です。

日本人なら専守防衛です。

自分からは手を出さない。

ただし自衛のための戦力は保有するのです。

専守防衛のポリシーに則り、自プロセス内だけで、CreateRemoteThreadを何とかしましょう。



スレッドってどうやって呼ばれるんだろう?

デバッガで呼び出し履歴などを見ていくと、 Thread は、以下のメソッドから呼ばれています。

kernel32.dll!@BaseThreadInitThunk@12()  + 0x12 バイト	
ntdll.dll!___RtlUserThreadStart@8()  + 0x27 バイト	
ntdll.dll!__RtlUserThreadStart@8()  + 0x1b バイト	

つまり、ここのどこかに処理を噛ますことが出来れば、スレッド実行に干渉できるわけです。

今回は、 BaseThreadInitThunk をフックしてみます。

やってみよう。
DWORD WINAPI USOThread(LPVOID lpParameter)
{
	MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0);
	return 0;
}


SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
			("kernel32.dll","BaseThreadInitThunk"),(LPVOID a1,LPVOID a2 ))
{
	void * _ecx = NULL;
	void * _edx = NULL;
	_asm
	{
		mov _ecx , ecx; //何かのフラグに利用しているらしい
		mov _edx , edx; //スレッド内で実行する関数のアドレス
	}

    //ここで嘘の関数を指定できる。
	_edx = (void*)USOThread;

	//一時的にフックを解除
	SEXYHOOK_UNHOOK();
	_asm
	{
		push a2;
		push a1;

		mov  ecx,_ecx;
		mov  edx,_edx;

	    //元の関数を呼び出す
		call CallOrignalFunction;
	}


}
SEXYHOOK_END_AS() g_BaseThreadInitThunk;

これで、スレッドの動作開始に干渉できます。

が、これには一つ問題があります。


sexyhookでは、UNHOOKするときにフックしたメモリ空間を元に戻しているのですが、ここはスレッドで動作しています。

メモリは一つしかないのに、スレッドは複数個ある可能性があります。

つまり、スレッド競合の問題が発生します。

同時に、メモリを書き換えることがあるかもしれません。


よって、ちょっとしたハックを行います。

DWORD WINAPI USOThread(LPVOID lpParameter)
{
	MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0);
	return 0;
}


SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
			("kernel32.dll","BaseThreadInitThunk"),(LPVOID threadParama,LPVOID nazo ))
{
	//このレジスタの値は壊してはいけない
	void * _ecx = NULL;
	void * _edx = NULL;
	_asm
	{
		mov _ecx , ecx; //何かのフラグに利用しているらしい
		mov _edx , edx; //スレッド内で実行する関数のアドレス
	}
	//選別ルーチン
	_edx = (void*)USOThread;

	//オリジナルの関数 +5 はsexyhookが破壊している領域を飛ばすためアドレスをずらす.
	void* orignalFunction = (void*)(((unsigned long) sexyhookThis->getOrignalFunctionAddr() ) + 5);
	
	//なりジナルの BaseThreadInitThunk を呼び出します。
	_asm
	{
		push nazo;
		push threadParama;

		mov  eax,orignalFunction;    //ebp esp を下で書き換えるため ローカル変数にアクセスできなくなるので、eaxに保持.
		mov  ecx,_ecx;
		mov  edx,_edx;

		//この関数をなかった事にしたいので、 スタックフレームを復元する.
		mov esp,ebp
		//ebpの復元
		pop ebp

		//BaseThreadInitThunk のプロローグ
		mov         edi,edi  
        push        ebp  
        mov         ebp,esp  

		//元のルーチンに飛ばす.
		jmp         eax 
//74AB338F  jne         @BaseThreadInitThunk@12+15h (74AB620Ah)    ここに飛ばす.
	}
}
SEXYHOOK_END_AS() g_BaseThreadInitThunk

sexyhookにアンフックさせるのではなく、自分の手でメモリアドレスを計算してプロセスをつないでいます。

f:id:rti7743:20111220224040j:image

これでスレッド競合の問題はなくなります。

本当はsexyhookにこの機能を搭載したいのですがねー。

そして完成へ

まとめます。


DLL INJECTION だけを防止するコードはこんな感じです。

//LoadLibrary* 系すべてでバリアを使う.
SEXYHOOK_BEGIN(int,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
		("NTDLL.dll","LdrLoadDll"),(PWCHAR PathToFile,ULONG Flags,PUNICODE_STRING ModuleFileName,PHANDLE ModuleHandle))
{
	//気に食わないdllはロードさせない. 
	if (wcsstr(ModuleFileName->Buffer ,L"ドS" ) != NULL)
	{
		//ドSって入っているから、ドSくんのdllだろうから、ロード拒否.
		MessageBox( g_MainWnd , "ドSくんからDLLを挿し込まれそうになったので防御しました" , "バリアー" , 0);

		return 0; //ロード禁止
	}

	//それ以外はふつーにロードするよ

	//一時的にフックを解除
	SEXYHOOK_UNHOOK();
	//元の関数を呼び出す
	return CallOrignalFunction(PathToFile,Flags,ModuleFileName,ModuleHandle);
}
SEXYHOOK_END_AS() g_LdrLoadDllHook;

CreateRemoteThread だけを防止するコードはこんな感じです。

DWORD WINAPI USOThread(LPVOID lpParameter)
{
	MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので無力化しました。\r\n" , "CreateRemoteThread検出" , 0);
	return 0;
}


SEXYHOOK_BEGIN(void,SEXYHOOK_STDCALL,SEXYHOOK_DYNAMICLOAD
			("kernel32.dll","BaseThreadInitThunk"),(LPVOID threadParama,LPVOID nazo ))
{
	//このレジスタの値は壊してはいけない
	void * _ecx = NULL;
	void * _edx = NULL;
	_asm
	{
		mov _ecx , ecx; //何かのフラグに利用しているらしい
		mov _edx , edx; //スレッド内で実行する関数のアドレス
	}
	//選別ルーチン

	//VirtualAlloc されたメモリの判別が難しい。
	//とりあえず、 VirtualAllocする人は、 PAGE_EXECUTE_READWRITE していたりしたら、 コード領域なのにWRITEができるーっていうことで落とすことにした。
	//相手が対策してきたら無意味なので、、、根本的な対処を考えないといけない。
	if (!IsBadWritePtr(_edx , 1))
	{
		//嘘のスレッド関数を渡してあげる。
		_edx = (void*)USOThread;
	}

	//オリジナルの関数 +5 はsexyhookが破壊している領域を飛ばすためアドレスをずらす.
	void* orignalFunction = (void*)(((unsigned long) sexyhookThis->getOrignalFunctionAddr() ) + 5);
	
	//なりジナルの BaseThreadInitThunk を呼び出します。
	_asm
	{
		push nazo;
		push threadParama;

		mov  eax,orignalFunction;    //ebp esp を下で書き換えるため ローカル変数にアクセスできなくなるので、eaxに保持.
		mov  ecx,_ecx;
		mov  edx,_edx;

		//この関数をなかった事にしたいので、 スタックフレームを復元する.
		mov esp,ebp
		//ebpの復元
		pop ebp

		//BaseThreadInitThunk のプロローグ
		mov         edi,edi  
        push        ebp  
        mov         ebp,esp  

		//元のルーチンに飛ばす.
		jmp         eax 
//74AB338F  jne         @BaseThreadInitThunk@12+15h (74AB620Ah)    ここに飛ばす.
	}
}
SEXYHOOK_END_AS() g_BaseThreadInitThunk

SetWindowsHookEx , CreateRemoteThread の両方に対応しようとすると、この2つを実装します。

片方だけでは、それぞれしか防ぐことができません。

このコードで、自分のプロセスを邪悪なフックから守ることが出来ました。

フックするプログラムがどんな手でこようと、フックされることはないでしょう。(カーネルサイドでのフックを除く?)


デモ

サンプルプログラム 「すべてを実装したドMやられ役_Anti_ALLHook無敵のバリア」がすべてのドS攻撃に耐えていることを実演。

f:id:rti7743:20111220223032p:image

こいつもうドMぢゃない。


攻性防壁

さて、フックから自プロセスを守ることはできたのですが、やられぱなしもイヤですから、何か反撃の方法を考えてみましょう。

攻性防壁みたいなもんです。


我々をフックしようとした代償がいかに高くつくかを相手のプロセスに思い知らせてやりましょう。



しかし、これが結構難しいのです。

まず、反撃するには、攻撃した相手が分からないといけないのですが、フックの場合、誰にやられたかという情報を得ることがすごく難しいのです。


DLL INJECTIONでは、挿入されようとしたDLLのファイル名がわかります。

なのでこのDLLの情報、製作者やファイルのパス名から、やり返す方法はあるかもしれません。

ですが、DLL INJECTIONを指示した直接のプロセス名はわかりません。

推測することしかできないのです。

また、挿入されたようとしたDLLをむやみに削除するプログラムを作るのは、あまりおすすめしません。

フックDLLはシステムに深く食い込んでいる可能性があり、ヘタにフックDLLを消去すると、出来の悪いフックプログラムによっては、システム全体が不安定になる可能性があります。

よって、ここではフック仕掛けてきたDLLがある場所にあるexeでドSくんのものだったら、そのプロセスを終了する(TerminateProcess)するようにしてみました。


CreateRemoteThread では、相手の名前すらも何もわかりません。

ただ、ちょっとだけ嫌がらせをすることはできます。

CreateRemoteThread を呼ぶルーチンは以下のようになっていることがあります。

HANDLE remoteThreadHandle;
remoteThreadHandle = ::CreateRemoteThread( processHandle, NULL, 0,	
				(LPTHREAD_START_ROUTINE) remoteCodeMemory, 
				remoteDataMemory, 0, NULL );

//注入スレッドが終了するまで待ちます.
::WaitForSingleObject( remoteThreadHandle, INFINITE );

ふつーのフックルーチンだと、フック処理は一瞬で終わるので、マルチスレッドをまともに実装するのではなく、こーゆー手抜きな実装になっていることが多いのです。

もし、CreateRemoteThread されたスレッドが長時間応答を返さなかったら、どうなるでしょうか?


フックしたプロセスフリーズしますね。

と、いうわけで、 CreateRemoteThread してきたプログラムには、 3日間寝とけ Sleep(1000 * 60*60*24*3) とでも返してあげましょう。

DWORD WINAPI USOThread(LPVOID lpParameter)
{
	MessageBox( g_MainWnd , "CreateRemoteThreadを検出したので、相手を冬眠させます。\r\n" , "お休み" , 0);
	::Sleep(1000 * 60 * 60 * 24 * 3);
	return 0;
}

f:id:rti7743:20111220224159j:image

これで、ぐっすりオネンネしてくれます。

余計な悪さはできないでしょう。


デモをやってみましょう。

サンプルプログラム 「ドMやられ役_Anti_Hook_ReactiveBarrier攻性防壁」と各フックプログラムです。

f:id:rti7743:20111220223926j:image

f:id:rti7743:20111220223924j:image

(なお、反撃するコードなどは法令を守って楽しく実装しましょう。)

まとめ

アンチフックについて解説しました。

また、余興としてフックしてきた奴らにやり返す攻性防壁を作って見ました。


無数のマルウェアチートツールなどのフックプロセスがうごめく今日。

メモリ空間はバトルフィールドなのです。

付録

サンプル一式をダウンロード。(VS2010 + windows7 64bitエディションで作成 home と ultimateで動作)

参考

別のプロセスにコードを割り込ませる3つの方法

Usaful anti dll injection function.

アナライジング・マルウェア

2011-12-15

C# で日本語合成音声・音声認識をやってみよう。

この記事は C# advent calendar 2011の記事です。

microsoftって音声認識・合成音声のOS組み込みをXPから初めています。

今回は、そのライブラリを使い、日本語合成音声でしゃべり、日本語の音声認識するソフトの作り方を説明したいと思います。

f:id:rti7743:20111216005330j:image


サンプルの使い方とソース

サンプルソース

http://rtilabs.net/files/2011_12_16/speechplatformtest_src.zip

サンプルexe

http://rtilabs.net/files/2011_12_16/speechplatformtest.zip

開発環境

windows7 64bit .Net4

依存ライブラリ

Speech Platform ver11

http://mahoro-ba.net/e1541.html

http://www.microsoft.com/download/en/details.aspx?id=27224

http://www.microsoft.com/download/en/details.aspx?id=27225



f:id:rti7743:20111216005330j:image

読み上げを押すと、読み上げます。

音声認識により「りんごください」と「バナナください」、「私がムスカ大佐だ」「らんらんるー」のいづれかの発音にマッチします。

ルールベースのマッチングなので、多少変なマッチでも、この4つのうちのどれかになります。


日本語合成音を手に入れよう。

windows7 64bitでは日本語合成音声は無料で公開されて来ませんでした。

しかし、今年の10月になって、ようやく無料での日本語合成音声が公開されました。


こちらからダウンロードできますが、やり方が少々めんどいので、

インストール方法は、まほろばさんのページを参考にしてください。

http://mahoro-ba.net/e1541.html


MS公式はこちらになります。

http://www.microsoft.com/download/en/details.aspx?id=27224

http://www.microsoft.com/download/en/details.aspx?id=27225



ラピュタ王家は地上に降りて二つに分かれたそうですが、MSの音声関係のライブラリSAPI と Speech Platform という2つのライブラリに分離してしまっており、単純に利用するのは少々メンドイです。


SAPI だと日本語の合成音声はやや微妙でした。しかもwindows7 64bitだと無料で使えません。(たぶん)

逆に Speech Platform だと、10月に公開された、結構綺麗に喋る日本語合成音声 haruka があります。

ただし、SAPI と比べるとやや癖があるので、音声認識の方で苦労します。

(うちでは未だに Speech Platform で dictication が動作しません。 SAPI5.4だと動くのに。 どうすれば動くようになるんでしょう・・・)


C#での音声ライブラリと罠。

windows7では音声認識OSに直結されました。(vistaから?)

そして、C#やその他の言語から普通に使おうとすると、実用的に使えない残念なものとなりました。


何が残念なんでしょうか?


ふつーC#でやろうとすると、System.Speech.Recognitionなどの .NETのライブラリを経由すると思います。

これは罠です。使ってはいけません

//これはダメな方のコードです

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Speech;

namespace test1234
{
    public partial class Form1 : Form
    {
        System.Speech.Recognition.SpeechRecognizer Reco = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Reco = new  System.Speech.Recognition.SpeechRecognizer();
            var chooices = new System.Speech.Recognition.Choices();
            chooices.Add("りんごください");
            chooices.Add("バナナください");

            var gram = new System.Speech.Recognition.Grammar(chooices.ToGrammarBuilder());
            gram.Enabled = true;

            this.Reco.MaxAlternates = 1;
            this.Reco.LoadGrammar(gram);
            this.Reco.SpeechRecognized += SpeechRecognized;


            this.Reco.Enabled = true;
        }
        void SpeechRecognized(object sender, System.Speech.Recognition.RecognitionEventArgs e)
        {
            Console.WriteLine(e.Result.Text);
        }
    }
}

奴が来る

このようなコードを使って、実行してみましょう。

何が起きるでしょうか?

f:id:rti7743:20111216005641j:image

おや、頼んでもいないのに変なウィンドウが登場しました。

しかも、ウィンドウ登場時に一定秒間OSが固まりましたネ。

さらに、こいつ動作が鈍く、たまにOS全体を巻き込んでフリーズしたりクラッシュしたりしてくれます。

さらに、音声認識を利用していると、長時間時間が経つと勝手に認識を打ち切って、スリープモードみたいな感じになるという、えっ!?という動作をしてくれます。

まったく使い物になりません。



なんということでしょう

windows xp とかだとこんなことはなかったのに。


windows7 (もしくはvista)から仕様が改悪されてしまいました。

microsoftはなんでこんなことひどい事をするのか・・・・

(もし、これをどうにか出来る方法をご存知の人は教えてください)


.NETバージョンは使い物にならない。COMを使おう。

音声認識ライブラリはCOMインターフェースを実装されています。

そして、大きく2つのインターフェースをもっていて、一つはSpSharedRecognizerでさっきの奴を呼んでしまう方法、もうひとつはSpInprocRecognizerでアプリ内で閉じてくれる方法です。

SpSharedRecognizer奴が来る
SpInprocRecognizerアプリ内で閉じて幸せ

SpInprocRecognizerでアプリ内で閉じてくれる方法だと、初期化もほぼ一瞬ですし、音声認識エンジンがおかしくなったとしてもアプリだけが落ち、OSを不安定にもしません。しかも、長時間稼働させても勝手にオフになるなどのおかしな動作もしません。ちゃんと動いてくれるライブラリです。これは使わない手はありません。



(windows7音声認識エンジンを改悪したチームは何でこんなコトしたの?)



.NETの音声認識・合成音声ライブラリを使うと、前者のアレな方を呼んでしまいます。

そこで、今回は、COMを利用して、アプリ内で閉じてみます。


Visual StudioC# を使うと COM も簡単に操作できるので楽ちんです。

試しにやってみましょう。


Visual Studioでメニューの「プロジェクト」から「参照の追加」を選択します。

f:id:rti7743:20111216010133j:image

次に、 「COM」 のタブを選択し、 Microsoft Speech Object Library を選択します。

もし、 Microsoft Speech Object Library が2つある場合、今回は バージョンが11.0 の方を入れます。

f:id:rti7743:20111216010203j:image

以上で、参照設定への追加は終わりです。

あとは、これを利用したコードを書くだけです。

ライブラリは、 SpeechLib の名前空間の下に展開されています。


合成音声で読みあげてみよう。

f:id:rti7743:20111216010355j:image

SpeechLib.SpVoice クラスの Speak メソッドを呼び出すと、コンピュータが喋ってくれます。

これをコードにすると、こんな感じです。(抜粋です。)

//using System.Speech;  //こっちは邪悪なので使ってはならぬ。
using SpeechLib; //for ver11

namespace speechplatformtest
{
    public partial class Form1 : Form
    {
        //合成音声ライブラリの読み込み
        private SpeechLib.SpVoice VoiceSpeeach = null;

        public Form1()
        {
            //合成音声エンジンを初期化する.
            this.VoiceSpeeach = new SpeechLib.SpVoice();
            //合成音声エンジンで日本語を話す人を探す。(やらなくても動作はするけど、念のため)
            bool hit = false;
            foreach (SpObjectToken voiceperson in this.VoiceSpeeach.GetVoices())
            {
                string language = voiceperson.GetAttribute("Language");
                if (language == "411")
                {//日本語を話す人だ!
                    this.VoiceSpeeach.Voice = voiceperson; //君に読みあげて欲しい
                    hit = true;
                    break;
                }
            }
            if (!hit)
            {
                MessageBox.Show("日本語合成音声が利用できません。\r\n日本語合成音声 MSSpeech_TTS_ja-JP_Haruka をインストールしてください。\r\n");
            }
        }

        //読み上げボタン
        private void YondeneButton_Click(object sender, EventArgs e)
        {
            if (this.VoiceSpeeach.Status.RunningState == SpeechRunState.SRSEIsSpeaking)
            {
                //現在話し中..
                return;
            }

            this.VoiceSpeeach.Speak(StringComboBox.Text , SpeechVoiceSpeakFlags.SVSFlagsAsync | SpeechVoiceSpeakFlags.SVSFIsXML);
        }



ピッチや速度・音量の調整もできます。

しかし、これまた不思議仕様がありまして、合成音声クラスには、ピッチの調整はあれど速度を調整するプロパティがありません。

//抜粋。
    public interface ISpeechVoice
    {
        [DispId(5)]
        int Rate { get; set; }     //ピッチレート

        [DispId(6)]
        int Volume { get; set; }   //音量
        
        
        //あれ? 話し速度を変える speed がないよ・・・・
        //気になる人は探してみよう。
    }

どうしてそういう設定にしたのか激しく謎ですが、こうなっています。

クラスのプロパティはこうなっていますが、音声読み上げの文字列の中にxml形式で制御できるようになっています。


具体的にはこんな感じで、指定ができます。

こんにちは世界。貴方の予想に反して、この音声が聞こえるでしょうか?
こんにちは世界。<volume level="10">大きな声で、こんにちは世界。</volume>元の声で、こんにちは世界。
こんにちは世界。<rate speed="5">早目に、こんにちは世界。</rate><rate speed="-5">遅めに、こんにちは世界。</rate>元の速度で、こんにちは世界。
こんにちは世界。<pitch middle="5">ピッチをあげて、こんにちは世界。</pitch><pitch middle="-5">ピッチを下げて、こんにちは世界。</pitch>ピッチを元に戻して、こんにちは世界。
こんにちは世界。<silence msec="500"/>こんにちは世界。沈黙をつくれる。怨念がおんねん<silence msec="500"/>

詳しい仕様はmicrosoftのサイト御覧ください。

http://msdn.microsoft.com/en-us/library/ee431815(v=vs.85).aspx


一部指定しても動作しないパラメータがあります。

サンプルのコンボボックスにいくつか登録してあるので、読み上げ本を押すと、どんな感じになるのかわかります。



音声認識させてみよう。

f:id:rti7743:20111216010607j:image


次に音声認識させてみます。

こちらは、windows7とかだとOSに標準で組み込まれています。

しかし、先ほどの「お話くださいウィンドウ」の罠があるので、こちらもCOMを経由して触るのが得策です。


りんごとばななの聞き取りをやってみましょう。

//using System.Speech;  //こっちは邪悪なので使ってはならぬ。
using SpeechLib; //for ver11

namespace speechplatformtest
{
    public partial class Form1 : Form
    {
        //音声認識オブジェクト
        private SpeechLib.SpInProcRecoContext RecognizerRule = null;
        //音声認識のための言語モデル
        private SpeechLib.ISpeechRecoGrammar RecognizerGrammarRule = null;
        //音声認識のための言語モデルのルールのトップレベルオブジェクト.
        private SpeechLib.ISpeechGrammarRule RecognizerGrammarRuleGrammarRule = null;

        public Form1()
        {
            //ルール認識 音声認識オブジェクトの生成
            this.RecognizerRule = new SpeechLib.SpInProcRecoContext();
            hit = false;
            foreach (SpObjectToken recoperson in this.RecognizerRule.Recognizer.GetRecognizers()) //'Go through the SR enumeration
            {
                string language = recoperson.GetAttribute("Language");
                if (language == "411")
                {//日本語を聴き取れる人だ
                    this.RecognizerRule.Recognizer.Recognizer = recoperson; //君に聞いていて欲しい
                    hit = true;
                    break;
                }
            }
            if (!hit)
            {
                MessageBox.Show("日本語認識が利用できません。\r\n日本語音声認識 MSSpeech_SR_ja-JP_TELE をインストールしてください。\r\n");
            }

            //マイクから拾ってね。
            this.RecognizerRule.Recognizer.AudioInput = this.CreateMicrofon();

            //音声認識イベントで、デリゲートによるコールバックを受ける.

            //認識の途中
            this.RecognizerRule.Hypothesis +=
                delegate(int streamNumber, object streamPosition, SpeechLib.ISpeechRecoResult result)
                {
                    string strText = result.PhraseInfo.GetText(0, -1, true);
                    this.HypothesisTextBox.Text = strText;
                };
            //認識完了
            this.RecognizerRule.Recognition +=
                delegate(int streamNumber, object streamPosition, SpeechLib.SpeechRecognitionType srt, SpeechLib.ISpeechRecoResult isrr)
                {
                    string strText = isrr.PhraseInfo.GetText(0, -1, true);
                    this.RecognitionTextBox.Text = strText;
                };
            //ストリームに何かデータが来た(?)
            this.RecognizerRule.StartStream +=
                delegate(int streamNumber, object streamPosition)
                {
                    this.HypothesisTextBox.Text = "";
                    this.RecognitionTextBox.Text = "";
                };
            //認識失敗
            this.RecognizerRule.FalseRecognition +=
                delegate(int streamNumber, object streamPosition, SpeechLib.ISpeechRecoResult isrr)
                {
                    this.RecognitionTextBox.Text = "--ERROR!--";
                };

//            this.RecognizerRule.CmdMaxAlternates = 0;

            //言語モデルの作成
            this.RecognizerGrammarRule = this.RecognizerRule.CreateGrammar(0);

            this.RecognizerGrammarRule.Reset(0);
            //言語モデルのルールのトップレベルを作成する.
            this.RecognizerGrammarRuleGrammarRule = this.RecognizerGrammarRule.Rules.Add("TopLevelRule",
                SpeechRuleAttributes.SRATopLevel | SpeechRuleAttributes.SRADynamic);
            //文字列の追加.
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "りんごください");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "バナナください");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "らんらんるー");
            this.RecognizerGrammarRuleGrammarRule.InitialState.AddWordTransition(null, "私がムスカ大佐だ");

            //ルールを反映させる。
            this.RecognizerGrammarRule.Rules.Commit();

            //音声認識開始。(トップレベルのオブジェクトの名前で SpeechRuleState.SGDSActive を指定する.)
            this.RecognizerGrammarRule.CmdSetRuleState("TopLevelRule", SpeechRuleState.SGDSActive);
        }

        //マイクから読み取るため、マイク用のデバイスを指定する.
        // C++ だと SpCreateDefaultObjectFromCategoryId ヘルパーがあるんだけど、C#だとないんだなこれが。
        private SpeechLib.SpObjectToken CreateMicrofon()
        {
            SpeechLib.SpObjectTokenCategory objAudioTokenCategory = new SpeechLib.SpObjectTokenCategory();
            objAudioTokenCategory.SetId(@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v11.0\AudioInput", false);
            SpeechLib.SpObjectToken objAudioToken = new SpeechLib.SpObjectToken();
            objAudioToken.SetId(objAudioTokenCategory.Default, @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Speech Server\v11.0\AudioInput", false);
            return objAudioToken;
        }
    }
}


イベントがデリゲートで実装されているので、C#との相性は抜群です。

余談ですが、C++からやると、EventObject や WindowMessage通知などなどの手法が選択できます。

C#だとそーゆーのはなしで、デリゲートで十分カバーできますし、旧来の手法よりも使いやすくて素晴らしい感じです。

正規表現音声認識

もっと、複雑な認識を SAPI XML形式で書くことができます。

<?xml version="1.0" encoding="UTF-8"?>
<GRAMMAR>
<RULE name="S" toplevel="ACTIVE">
  <L>
    <P>りんごください</P>
    <P>バナナください</P>
  </L>
</RULE>
</GRAMMAR>

共通部分の、くださいをくくりだして、以下のようにもいけるはずです。

<?xml version="1.0" encoding="UTF-8"?>
<GRAMMAR>
<RULE name="S" toplevel="ACTIVE">
  <L>
    <P>りんご</P>
    <P>バナナ</P>
  </L>
  <L>
    <P>ください</P>
  </L>
</RULE>
</GRAMMAR>

SAPI XMLは一応簡単にかけるのですが、長くなると結構めんどいです。


なんつーか、正規表現音声認識のマッチパターンがかければいいのに・・・って思いませんか?

私もそう思ったので作って見ました。



正規表現で音声認識です。


私の正規表現音声認識ライブラリを使うと、りんごとばななにマッチすることをいかのように書けます。

(りんご|バナナ)ください

まとめ

C#で 合成音声・音声認識 を利用する方法について説明しました。

COMなので、C++や、その他の言語からも利用できます。

ただしwindows縛りになってしまいますけどね。


うちではこれを利用して家電制御アプリを使っています。(こっちの実装はC++ですけど)

家電を音声でコントロールする。

http://d.hatena.ne.jp/rti7743/20110828/1314546712

イオナズンして家電制御

http://d.hatena.ne.jp/rti7743/20110830/1314663041


近年、合成音声・音声認識はやっと実用化され、身近な存在になりつつあります。

siriに代表されるような、音声エージェントも登場しましたし、さらにブレークしていくものだと思います。


音声でコンピュータコミニュケーションをとれる未来が来たらきっと楽しいと思います。

音声認識や合成音声をガチで作ろうとすると難しい数式の嵐になると思いますが、無料でつかえるライブラリがあるわけですから、こいつらをうまく活用して、楽しい応用アプリがたくさん生まれれば未来はもっと楽しくなるでしょう。