PHPのシステムをHHVMに載せた時に動かなくて直した所一覧

PHPの既存システムをHHVMに載せ、その後サービスを維持しつつHackに変え、静的型付けやモダンな書き方により、堅牢なものに変えていくことを目的に試験環境を構築しハマりつつも大体動いたレベルの所まで行ったのでここに残しておく。

phpconには行ってないけどHHVMの注目度も上がりそうだし役に立つかもなので今日書く事にしました。

まずやったこと

  1. 既存PHPプロジェクトをDocker上に展開できるように用意
    何度も試行錯誤することが目に見えていたため、環境構築・再現がしやすい事が必須と考えた
  2. HHVMをDocker上でビルドできるように
    Ubuntuだとそれ程困らないが、元の環境がCentOSだったため、自分でビルドするものを用意した。元はCentOS6系だったが残念ながらそちらでのビルドはハマりどころが多すぎたためCentOS7にした。githubとDocker hub両方に上げておいた。


    suzuki0keiichi/docker_centos7_hhvm · GitHub


    suzuki0keiichi/centos7_hhvm Repository | Docker Hub Registry - Repositories of Docker Images

    Dockerfileの方はRUNが大量に書いてあるビルドスクリプト同様のものなので、Docker上で動かさない場合でもビルドするのには役に立つかも。
    10/12追記 静的解析などに使用するhh_serverコマンドは$USERが設定されていないと動かないため、Dockerで起動したそのまま(root)の状態だと実行できないので気をつける。
  3. 1で出来たもののPHP部分だけを2に差し替え
  4. httpdとかの設定を追加
    mod_rewriteとmod_phpでのベタな構成だったため、追加でProxyPassMatchを入れるだけで良かった。

 ProxyPassMatch ^/(.+.(hh|php)(/.*)?)$ fcgi://127.0.0.1:9000/PHPSOURCEROOT/$1

はまりどころ

「不具合まで再現する」をポリシーとしてPHPを再現しているHHVMだが、残念ながらまだ差異がある所がある。

ここからの説明では「さすがにそんなコードを書くほうが悪い」と思われる所が出て来るかもですが、旧石器時代且つ自分が書いたものではないのでお察し下さい、、、

大きく分けると際のある場所には32種類あった。

  • 各組み込み型、関数の微細な挙動の違い
  • 組み込み型や組み込み型を継承した型のオブジェクトのシリアライズ、get_object_vars等のオブジェクト全体に対する操作
  • evalやpreg_replaceなどの中でのevalが実行されるスコープの違い 10/13修正
各組み込み型、関数の微細な挙動の違い

今回出会ったのは幸い一件だけだった。

2件の問題に遭遇した。

一件目はfile_get_contentsでhttpヘッダーを渡す際にヘッダーの改行文字の扱いで、PHPの場合はCRLFでもLFでも正常にヘッダーが送信されるが、HHVMの場合ではLFだとヘッダーのフォーマットがおかしくリクエスト先サーバーにBad Requestを返されてしまう。


hhvm/http-stream-wrapper.cpp at 07f8201682818abdcc0aa581c9eaa991b2b04965 · facebook/hhvm · GitHub

この辺りが問題と思われるので、報告すれば修正してもらえるかも。

英語コミュニケーションの億劫さであんまり自分でやる気がない、、、

 

二件目はpreg_replaceで/eした場合のevalが実行されるスコープの違いだった。

(preg_replaceの/eは現在非推奨となっている)

通常のevalの方はselfもローカル変数も正しく参照できた。

PHPの場合はpreg_replaceが動いた箇所によってselfが何を指しているかちゃんと理解されたり、外で宣言されているローカル変数だったりを使うことが出来る。

HHVMの場合はpreg_replaceでselfを指定するとphpレベルで解決できない旨の警告メッセージが表示される。また、ローカル変数も見つけられなかったようなので、とりあえずstatic変数に入れるという不安な形で実現させた。

動くには動くが、非常にモヤモヤするので解決策を探して行きたい。

当初この差異はeval系全般における問題と勘違いして別カテゴリで書いていたが、実際にはpreg_replaceのみに起こる問題だったため関数個別の問題のカテゴリに移しました。

組み込み型や組み込み型を継承した型のオブジェクトのシリアライズ、get_object_vars等のオブジェクト全体に対する操作

全部の組み込み型かは不明。今回あったのはSimpleXMLElementとDOMDocumentだった。

serialize関数を呼んだ際にphpのエラーログとして通知してくれるケースもあれば、unserializeの時に何故か抜け落ちるケースも有る。後者は特定がかなり難しい。

しかし問題箇所さえ分かってしまえば対応は結構簡単で、__sleepと__wakeupの際に上手く組み込み型を保存しなくても済む状態にすれば良い。

継承してしまっていてコードの変更が大変そうに見える場合でも、privateなプロパティーとして持つように変更し、__callで中継してやることで影響箇所を小さくすることが出来る。

 

大体こんな事をやったら今回実験対象だったシステムは動くようになった。

これから先に同じことをやろうとする人たちのお役に立てば幸いと言うか、ぜひHHVMに乗り換えていってもらってHHVMをFacebookと一蓮托生じゃない感じにして頂けたらと思います。

技術的負債がいかに問題かをどう伝えるといいのか

沢山の方がチャレンジしては上手く行ったんだが行ってないんだがみたいな結果に終わるコレに自分もチャレンジしてみようと思う。
上手く行ってるか分からないのは自分が技術者だからなんだけど。

まず技術的負債って言葉で、何か悪いものがたまって行くのはわかると思うが、どのくらい深刻なのかは伝わらないよな。
それと、プログラムが日に日に複雑になって行くというのがなかなか他に似たようなケースがなく、何が大変なの?という感じなんだと思う。
特に少しだけ、小規模なプログラムをかじったことがある人には。
彼らはやり方を知ってる分アレを取ってきてアソコに出すだけでは?何が大変なの?とふざけた事を思うだろう。

プログラムをメンテし続けた人にしか分からないこの難しさを別の形でイメージしやすいもので伝えないといけないんだと思う。

ここからがチャレンジ。

まず、プログラムというのは矛盾があっては行けないルールブックのような、法律のようなものです。
矛盾があると、プログラムは止まったり壊れたりしてしまいます。
出来るだけ避けないといけないです。

なので、時間がなくて、その場を取り繕うような適当な、厳密に言うと嘘があるルールを定義してしまうと、後で矛盾が出やすくなってしまいます。

このまま突き進むなら、嘘をつき続けたり、嘘をつくための更なる嘘をつくことになります。
どんどん巨大なルールブックになって行くのに、過去に定義したルールも含めて少しの矛盾も許されなくて、その上で嘘を隠し通さなければいけません。
数カ月から数年に渡ってものすごい複雑な嘘を矛盾無くつき続ける、と考えるとかなり苦しいものであることがイメージ出来るんじゃないかと思います。

これが世に言う技術的負債です。

しかし、一つ目の嘘の後に訂正の機会をもらえると、"さっきは勢いでああ言っちゃったけど、正確には違うんだ。こう言う事なんだ。"と訂正出来ます。
その後は正直に、自然にルールブックにルールを追加して行くことが出来ます。
嘘がないルールブックなので、新しいルールが増えてもどこに影響するかが分かりやすいため、矛盾が起こりにくいです。

この訂正作業が世に言うリファクタリングだったり負債の返済です。

長い期間をかけてルールブックを大きくして行きたいなら、適宜この作業を組み入れた方が合理的と言えます。
たとえ1度も適当なルールを作ったことが無くても、過去に作ったルールが全て未来を予見して作ってる訳では無いので、新しいルールに合わせて訂正しないと嘘になるケースがあります。
この場合もやはり訂正の機会を適宜設けた方が良いと言えます。

ここまで。
大分乱暴なところもあるけど、厳密さより伝わりやすさを重視した。
どうかなーやっぱ伝わんないかなー。

Scala.jsで各種コードの変換結果を見てみる

どんな形で変換されるのかが知りたいケースがあったので調べた。

(主にjsのフレームワークのサンプルをScala.jsで書くとどうなるか等の用途)

変換後にはオブジェクト宣言部とかオーバーヘッド的なものが沢山生成されるが、今回の描きたい所ではないので省いてある。

valのメンバ変数がどのように変換されるか

defがどのように変換されるか

overloadがどのように変換されるか

scala.js.Arrayの操作がどのように変換されるか

ifの最後の評価結果の代入がどのように変換されるか

パターンマッチがどのように変換されるか

最初見た時からわかってたことではあるが、人間が読むコードではないなという感想。

便利だった赤ちゃんグッズを忘れないうちに書いておく

スイマーバ

赤ちゃんのクビにつける浮き輪。赤ちゃんがフワフワ浮いている状態になっていて可愛い。

お風呂に入れる際も親が両手を使えるので、実用面でも便利な模様。

ただ、ちょっとしたことで外れる可能性もあるので必ず目を離さないこと。

ミラーレス一眼 + Eye-Fi + Flickr(Proアカウント)

コンデジスマホのカメラを使っていた時は分からなかったのだけど、明らかに画質が違う。また、Eye-Fiを使って(ほぼ)容量無制限なサイトに上げられる環境が出来上がると、撮影する量も変わってくる。うちはNEXを買った。個人的にはタッチフォーカスが付いているものがおすすめ。レンズ沼には嵌らないよう、最低限のレンズだけで済ませること。Flickrは類似サービスのどれでも良いが、閲覧権限のデフォルトはprivateにしておいたほうが何も考えずに撮影できて良い。

バンボ

赤ちゃんを固定しつつ座らせておける椅子。すわり心地は悪くないらしく、あまり赤ちゃんも嫌がらない。椅子の前面にテーブルも付けられるため、食事もそのまま可能。水に濡れても大丈夫なのでお風呂にも入れておくことが出来る。

だっこ紐

買うのは当たり前なのだが、メーカーや製品によってかなり合う/合わないがあるので必ず試着すること。うちではエルゴというものにした。取り急ぎで買っていた(別に安物でもない)抱っこ紐が普通だと思っていた時にエルゴを試着して、密着度、安定度の違いに驚いた。

iPad + iBallz

高いが両親にも配布する事。特に奥さん側。Facetimeの設定まで終えておくと、何かある度に気軽に顔を見せられて良い。奥さんが赤ちゃんと触れている時間が長いうちが多いと思うので、その都度両親と連絡を取っていると赤ちゃんと両親も仲良くなったり、少しの間任せておけたりする。また、前述のFlickr等の閲覧アプリも設定しておくと、都度のイベント写真も自由に見せることが出来る。

iBallzはiPadに装着する衝撃吸収用のアクセサリで、iPadの四つ角にゴムボールをつけるジョブズなら卒倒するようなデザインをしている。赤ちゃんを持つ家ならその価値はあり、少し落としたり倒した程度では何も起きない。(商品サイトにデモ映像があるのでそちらを見たらわかると思います)

ウォーターサーバー

震災直後に不安を感じて導入。うちではアクアクララにした。会社自他はそこまで重要じゃないが、震災後の色々な不安に悩まされずに冷たい水、熱いお湯が出てくるのは便利。ミネラルが含まれるものは飲み過ぎた際に不安もあるため、ROのもので良かったと思っている。金額は結構するので、震災が無かった際も入っていたかは微妙。

業務用ドライヤー(2014/9/4 追記)

高出力&コードが長いため射程距離が長く、子供が自由に移動しても対応できる。また本体、コード共に丈夫な場合が多い。Amazonなどで探すと大体数個のメーカーに絞られるが、多分どれでも良い。うちではNobbyにした。

jberkel/android-app.g8を使ってハマった所メモ

大して今更書く成果は無いんだけど誰かのお役に立てば

  • Windowsでプロジェクト作成を行おうとすると死ぬ
    • ディレクトリ関係の問題の模様。自分のとこだけかも。
  • プロジェクトさえ別OSで作ってしまえばその後はWindowsでも使える
  • sbtの0.12系以上じゃないとsbt立ち上げ時にライブラリが見つからない
    • 設定方法自体が間違ってるかもだけど、sbt 0.11軽だと2.9.1の方を見に行ってしまいjarが見つからなかった。
  • JDK7だとproguardが対応してなくて死ぬ

Smarty側に特に手を入れずにQuercus上で動かしたった

前回話しに上げた変更の

  • property_existsの内部でissetFieldを使わせない
  • getFieldでnullの時に次の候補(staticとか__get)とかに行かせずにnullを返す

と言うものでまず最初の爆死がなくなり、次はnullオブジェクトに対してisTrustedTagを呼ぼうとして死、と言う部分だったがこの部分はissetを追っていった所null判定が単純に間違っていた事が分かった。

 

issetの中でもプロパティに対して行うものはissetFieldを呼ぶが、ここで比較しているnullが単純なNullValue.NULLとの!=になっていた。

isTrustedTagの件は実際に比較先はVarで、実際の値がNullValueであるため、比較した結果非nullと言う扱いになって関数を呼びだそうとし、死と言う流れになっていた。

 

Value型はeqと言う関数で比較するようになっているように見えるのと、PHPのissetに関しては値がnullの場合はfalseなはずなので、eqを使うほうが正しい。

この部分を試しに書き換えた所、Smarty側には何も手を入れず、簡単なテンプレートをエラー無く表示することができた。

 

Quercusって修正を送りたい場合どーすると良いんだろう。

 

今回Quercusの中身を見をみた感想としてはかなり統一感があって見やすい&探しやすいコードになっているように思えた。

githubでPull Request募集したら結構協力者いると思うんだけどなあ。

仕様だってPHPの完全互換を目指す部分は揉めないだろうし。

Quercus上でのproperty_existsと__get

ソースを追ってみたところ、property_existsの方はClassesModuleと言うクラスにあり、内部的にはissetFieldと言う関数を参照していた。

名前から察する通りisset系なのでnullの場合はfalseになるようになっていた。

修正するとしたらissetFieldではなくhasFieldとか別の関数を用意して、それを呼ぶようにしたほうが良いか。

 

また、プロパティがnullの場合に__getに来てしまう問題だが、これはObjectExtValueと言うクラスにあるgetField系の中身をどうにかしてやる必要がありそうだった。

unsetもしくはnullの場合次の候補へ(__getなどへ)回す処理になっていたため、今回のような非互換な挙動になっていた。

 

どちらも実験的に上記のような対応を入れてみたところ、残るはisTrustedTagと言う関数をnullオブジェクトに対して呼び出していると言うエラーが出るところまで来た。

こちらの問題はSmartyのコードを見てみると、issetでオブジェクトがfalseかまたはオブジェクトに対してisTrustedTagを呼ぶと言うもので特に問題なく、echoなどで中身を見てみるとissetの結果が正しくない事が分かった。

 

が、流石にissetが常に正しく動いてなければ互換性もクソも無いだろうと思って実験コードを書いたらやはり挙動は正しく、何か複雑な組み合わせの際に間違った値になってしまっているように見えた。

ちなみに、ここはQuercus側を回収せずissetを!= nullに変えることでその後のエラーを見てみたが、そこを回避できれば簡易的なテンプレートは正しく表示されるところまではいけた。

 

調べ始めて1〜2日で判明してしまうレベルの問題が3件もあったように見えるが、実際の所これは何で解決されて無いんだろう。

更新頻度は低いが放置してるとまでは行っていないように思えるんだけどな。

 

あとは最終的に正しい修正箇所がわかった場合にどういった形で修正依頼を出せばいいのか、と言うところだな。githubを使った形跡はあるんだけど、現在はsvnしかなさそうだし、、、