hhvmのExtension書いてみた
みなさんこんにちは。hackしてますか?
今日はhhvmのC++拡張(Extension)について書いてみます。
前振り
hhvmはfacebookが開発・公開しているPHPの処理系のうちの一つでC++で書かれており、linux上でのJITがサポートされており場合によってはとても高速にPHPアプリケーションを実行する事ができます。
勿論Native拡張を書くこともでき、既存のライブラリ資産の有効活用やどうしても速度が出ない部分の改善などが簡単に行えるので手段として知っていると便利です。
この記事をきっかけにhhvm Extensionのとっかかりになれば嬉しいです。
開発環境を作る
開発環境はlinuxの環境を整えましょう。
OSXやその他の環境でのビルドも対応しているのですがJIT未対応だったり予期せぬバグや地雷を踏む可能性が高いので積極的にOSXのバグフィックスを行ってフィードバックしていきたい、という場合以外はあまりお勧めできません。
もし、OSX上で開発を行う場合はVirtualBox等を使って仮想環境で開発をするのが問題が少なくお勧めです。
hhvmのビルドは時間がかかりますし、Debugシンボルが入ったバイナリは数G単位で肥大化するのでディスクサイズは最低でも40G、メモリやCPUは割り当てられるだけ割り当てたほうが良いです。
特に、Extensionを書く場合はDebug版を使わないとセグメンテーションフォールト等の問題が追いづらくなるのでDebug版でのビルドが必須となります。また、hphpize等の開発ツールもパッケージ版には含まれておりませんのでExtensionの開発をする場合は自分でビルドしたものを使う必要があります。
hhvmのDebugビルドを行う
私はDebianが好きなのでDebian 7で環境を構築します。詳しくはメインの情報源であるWikiを参考に関連パッケージのインストールを行ってください。
https://github.com/facebook/hhvm/wiki/Building-and-installing-HHVM-on-Debian-7
Extensionの開発ではDebug版使いたいのでcmakeのビルドタイプをDebugに指定します。
1 2 3 4 5 |
# Building HipHop cd hhvm git submodule update cmake -DCMAKE_BUILD_TYPE=Debug . make |
makeの並列度を高めると私の環境では途中でビルドが失敗しまったので普通にビルドしています。
私のmacbook proでは全体のビルドが終わるまで1時間ほどかかるのでお昼に行く前や、寝る前にビルドするのがおすすめです。
実際にExtensionを書いてみる前に
ExtensionについてはWikiの情報やSara Golemonが書いてくれたexampleを見ると捗ります。
https://github.com/facebook/hhvm/wiki/Extension-API
https://github.com/hhvm/example
今までZendEngineを触ってきていた人であれば、m4で挫折し、マクロで挫折し、と挫折の階段を乗り越えていかないとExtensionが書けなかったのでcmake . && makeするだけで使えるのはなかなか感慨深いものがあります。
hhvmではC++で実装した関数とPHPへの関数への登録はHNI(Hhvm Native Interface)、というhackのAttribute付き記法を使います。
ZendEngineの場合PHPに出す関数やメソッドの引数はマクロで書いて、関連付けて、とステップが多かったのですがHNIの登場によりhackで関数のプロトタイプやバインドの指定が書けるようになりました。
PHP系の言語で書ける、ということは当然DocCommentもその場に書けるようになったということですので、Extensionのメンテナとしては長年面倒だったドキュメントの更新やパラメーター宣言が間違いなく確実に行えるようになった点はとても嬉しく思います。
Extensionを書いてみよう
むかーしかいたウノウラボのエントリではmigemoのExtensionを作ったのですが同じものをやるのもなんですし、最近私の中でwebsocketのframeを処理するのがマイブームなので、websocketのframeをparseする拡張のひな形を作ってみましょう。
実際にparse部分の説明まですると本題と大きくずれてしまうので今日の記事ではビルドして関数が実行できるだけの部分までを作ります。WebSocketのProtocolはRFC6455に記載の通りでざっくりいうとヘッダ付きのデータを1単位とした構造となっています。
実装自体も簡単な部類なので興味があるかたはぜひ一度RFC6455の実装をしてみてください。
config.cmake
cmakeに関しては特徴的な所そんなにないので特に書くことないです
1 2 3 4 |
SET(CMAKE_CXX_FLAGS_DEBUG "-g -pg -O0") HHVM_EXTENSION(websocketframe websocketframe.cpp) HHVM_SYSTEMLIB(websocketframe websocketframe.php) |
その他必要なライブラリなどがあれば通常のcmakeの使い方通り追加してもらえれば大丈夫です。
インターフェースの定義 - websocketframe.php -
HNIではNative関数やオブジェクトの実体への参考情報として<<__Native>>や<<__NativeData>>と呼ばれるAttributeを使います。下記のHNIの例ではPHPクラスのWebSocketFrameへの拡張情報として<<__NativeData("WebSocketFrame")>>というC++のクラスを登録する。各メソッドに関しては該当の名前に対応するプロトタイプの宣言を行う、といった意味で捉えてもらえばOKです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?hh <<__NativeData("WebSocketFrame")>> class WebSocketFrame { public function __construct() { } <<__Native>> public function getPayload() : string; <<__Native>> public function getOpcode() : int; <<__Native>> public static function parseFromString(string $bytes) : mixed; } |
ひとまずwebsocketのprotocolをparseする分にはこんな感じのインターフェースで十分かと思います。
因みにHNIはビルド時に参考情報として使われるのでコンパイル後にHNIだけ変更しても意味がありません。変更した場合は再度ビルドしましょう
ひな形の実装 - websocketframe.cpp -
今回はクラスへの拡張データの登録にNativeData Attributeを使っているのでhphp/runtime/base/base-includes.hの他にhphp/runtime/vm/native-data.hのインクルードが必要となります。NativeData Attributeとして扱える使えるクラスは空のコンストラクタ、コピーコンストラクタ、デストラクタが最低限必要となります。
Native::dataに関しての詳細はこのコミットログを参照してください。
それではソースコードいってみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
#include "hphp/runtime/base/base-includes.h" #include "hphp/runtime/vm/native-data.h" namespace HPHP { // websocket frameのparser class (NativeData attributeで指定したクラス) class WebSocketFrame { public: WebSocketFrame() {m_opcode = 1;}; ~WebSocketFrame() {}; WebSocketFrame& operator=(const WebSocketFrame& src) { return *this; } String GetPayload() { return String("Helo World"); } int64_t GetOpcode() { return static_cast<int64_t>(m_opcode); } static Class* c_WebSocketFrame; uint16_t m_opcode; }; const StaticString s_WebSocketFrame("WebSocketFrame"); Class* WebSocketFrame::c_WebSocketFrame = nullptr; // 関数実装 static Variant HHVM_METHOD(websocketframe, getPayload) { //HHVM_METHODは_thisがパラメーターとして与えられるのでメンバメソッドではこのようにしてNative::dataから情報が取れる WebSocketFrame* frame = Native::data<WebSocketFrame>(this_.get()); return frame->GetPayload(); } static int64_t HHVM_METHOD(websocketframe, getOpcode) { WebSocketFrame* frame = Native::data<WebSocketFrame>(this_.get()); return frame->GetOpcode(); } static Variant HHVM_STATIC_METHOD(websocketframe, parseFromString, const String& bytes) { if (!WebSocketFrame::c_WebSocketFrame) { // クラス定義を探す場合はHPHP::Unit::lookupClass(HPHP::StringData *data)を使う WebSocketFrame::c_WebSocketFrame = Unit::lookupClass(s_WebSocketFrame.get()); assert(WebSocketFrame::c_WebSocketFrame); } Object object = ObjectData::newInstance(WebSocketFrame::c_WebSocketFrame); return object; } // extension定義 static class WebSocketFrameExtension : public Extension { public: WebSocketFrameExtension() : Extension("websocketframe") {} virtual void moduleInit() { // メソッドの登録 HHVM_ME(websocketframe, getPayload); HHVM_ME(websocketframe, getOpcode); HHVM_STATIC_ME(websocketframe, parseFromString); // NativeDataの登録 Native::registerNativeDataInfo<WebSocketFrame>(s_WebSocketFrame.get()); loadSystemlib(); } } s_websocketframe_extension; HHVM_GET_MODULE(websocketframe) } // namespace HPHP |
上記のコードの通りC++拡張を作る場合はHPHP::Extensionを継承したクラスの実装とmoduleInitでの関数登録が最低限必要となります。
今回はHNIで定義したWebSocketFrameクラスに対してHPHP::WebSocketFrameクラスを拡張情報(NativeData)として登録し、WebSocketFrameの各メソッドの宣言をHPHP::WebSocketFrameExtension::moduleInitで行っている、ということですね。
ビルド 〜 実行
準備も整いましたし、実行してみましょう。
コマンドライン引数で拡張を読み込む方法もあるのですがconfigファイルの指定のほうが色々と便利なのでconfig.hdfを追加しています。
1 2 3 4 5 6 7 |
echo -ne '<?hh\n$f = new WebSocketFrame();\nvar_dump($f->getPayload());' > example.php echo -ne 'DynamicExtensions {\n\twebsocketframe = websocketframe.so\n}' > config.hdf hphpize cmake -DCMAKE_BUILD_TYPE=Debug . make hhvm -c config.hdf example.php string(10) "Helo World" |
意図したとおり実行できましたね。あとはたんたんとclassの実装をするだけの作業なので出来上がりは下記リポジトリを参考にしてください。
https://github.com/chobie/hhvm-websocketframe
まだまだ情報が少ないので間違っている所もあるかと思いますが、その際は教えていただけるととても嬉しいです。
hhvmのExtensionを書く場合は自分がやりたい機能がどの既存のPHP関数で実装されているかを調べれば大抵すぐにやり方がわかると思います。
ZendEngineではそこからさらにマクロやら内部実装を調べてトライアンドエラーを繰り返して、という流れだったのですがhhvmの場合は見通し良いですし、とりあえず動くのが簡単につくれるので一回トライしてみるのがオススメです。
おわりに
今日はhhvmのC++ Extensionの作り方について解説してみましたがいかがでしたでしょうか?
基本的にはSystemLibで十分速度的に賄えるのですが、今までメンテしてきたCやC++のライブラリを簡単にhhvmに組み込めるのはとても魅力的ですね。
hhvmはまだまだ過渡期なので情報がすぐ古くなってしまいます。この情報は2014/04/01時点の情報であり、今後大きく内容が変わることがありますので、hhvmの情報はコミットログを定期的にみつつ、常に新しい情報を得られるようにするのがオススメです。
おわりのおわりに
正直な所、私はhhvmの2系までは静的解析が高速に行える点はいいな、と思っていたのですが既存の拡張が使えないなどの理由からあまり乗り気ではありませんでした。
hack対応の3系の発表でちょこっとやる気が出始め、評価のためにuvのbinding作ってHeloを返すだけのHTTPサーバーを書いてたらmacbook proのVM上で10k/secとかでちゃったので、そりゃーもう変な脳汁でまくり。(これ自体のベンチは実際あまり価値ないんですが、大抵他のLL言語だとローカル環境で5k/secぐらいなのでなんか嬉しくなっちゃいますね)
実はZend compatibility layerなんてのもhhvmにあるんですが、そんなんマンパワーでどーにでもなるんでもっとcompatibilityのテスト増やして速くしたいですね。
PHPこれで勝つる!長かった漂白時代終わった!ということで今ではhhvmにゾッコンです。
そんなネタは置いておいて、今までPHPかCぐらいがメインで書く言語だったのがhhvmのおかげでC++を積極的に書く理由ができた事のほうが嬉しかったりします。
hhvmへのコードのcontributionにはCLAへのサインと送付が必要となります。テストコードや未実装部分等コミュニティの力を必要としておりますので興味があればぜひ参加してみてください。
それでは happy hacking!