WebAssemblyを使ったPHPのリアルタイムPlaygroundを作りました。
はじめに
こんにちは @glassmonekeyです。
久しぶりの投稿になります。
今回は個人開発ネタでWebAssemblyを使ったPHPのPlayground
を作ったので紹介になります。
PHPをWebAssemblyで動かす仕組みそのものに関しては
2023年3月23日にて
PHPerKaigi2023でPHPをブラウザで動かす技術も話す予定なので
そちらも合わせてご確認ください。この記事ではWebAssemblyそのものやPHPのBuildについてそこまで触れません。
この記事では主にサービスに関する実現手段にフォーカスしての紹介になります。
作ったもの
ソースコードはこちらになります。
なぜ作ろうと思ったのか
PHPで簡単なスニペットの実行と共有に課題を感じていたためです。
コードレビューといったコミュニケーションの場面や、文法の確認といった様々なスニペットの実行や共有へのニーズはあるのではないでしょうか。
私自身、普段はPHP以外にもGoを書くことがそこそこありまして、Goの場合だとGo Playground
でスニペットの実行と共有は簡単に行うことができます。
その点、PHPはこれといった決定的なPlayground
はなかったように感じます。
そこで、今回は以下の点にこだわって作ってみることにしました。
- リアルタイム性 ... 記述した内容がその場で描画されるように。
- シェアのしやすさ ... 編集した内容をURLに埋め込みし、URLから入力内容を再現できるように。
- PHPらしさ ... 本来PHPはHTMLのテンプレートエンジンの側面があったはずなので、HTMLもレンダリング可能なように。
特にリアルタイム性の観点からWebAssembly
を使うことにしました。
先行プロダクト
今回開発するにあたって、PHP + Assembly
が一番の困難な点だったので、 参考にさせていただいた先行プロジェクトを紹介しておきます。
oraoto/pib
おそらく元祖PHP + Assembly
のプロジェクトです。デモとしてPlayground
が用意されています。
このプロジェクトの存在を認知していたことが、今回作ったきっかけの大きな要因でした。
ただビルド環境を再現することに再現することが難しく、PHPの対応バージョンも7.4のみだったりと参考にするには課題が山盛りでした。
そのため、このプロジェクトはそこまで参考にせず後述するWordpress Playground
の方をベースに取り込みました。
とは言っても、Wasmのビルド方法の大半はoraoto/pib
からの源流を組んでいるものが多く、その大半が以下のPR起因なので詳細は不明な点が多かったりはします。
(良い子のみんなはNo description
はやめようね!!)
ちなみに認知したきっかけそのものは去年の PHP Conference Japan 2022で登壇した際の資料調査で見つけたことがきっかけだったりはします。
よかったらこれも見てくれると喜びます。
WordPress Playground
みなさんご存知WordPress
のPlayground
版でなんとブラウザで動きます。
ドキュメント周り充実しているので、一読してみるとWebAssemblyそのものの入門にいいかもしれません。
最近ビルドプロセスの改善が行われており、近内にWasmにbuildしたPHPを呼び出すためのnpmパッケージが公開されるようです。
ちなみにこの記事を執筆していた時点ではビルドプロセス部分をフォーク&ベンダリングして取り込むことをしました。
npmパッケージが公開されたらそれに乗り換える可能性は高いかなと思います。
実装詳細について
それでは簡単にPlaygroudそのもの実装部分の解説をします。
SPA予定だったのと、私自身React
の経験が乏しかったのでReact + TypeScript
で実装をしました。
また、扱うコンテンツが静的コンテンツのみなのでGit hubPages
にしました。
WebAssemblyのbuildについてと、BuildしたWebAssemblyを呼び出すReactアプリケーションについてそれぞれ説明します。
WebAssemblyのBuildについて
PHPをWebAssembly用にbuildもすこし触れてはおきます。
WebAssemblyへのbuild方法
PHPそのものはC言語なので、 コンパイラーにはemscriptenを用います。
詳しくは説明しませんが、WebAssembly経由でPHPを呼び出し可能にする
ためのエントリーポイント用C言語ファイルを含めてビルドすると
グルーコード含めて出力されます。
emscriptenの説明についてはMDNのC/C++ から WebAssembly へのコンパイルに詳しく載っているので詳しくはそちらをご覧ください。
WebAssembly用エントリーポイントの作成について
WebAssembly経由でPHPを呼び出すコードはこのような感じです。
char *code
はJavaScriptから渡されるPHPコードです。(e.g. <?php phpinfo();
)
実行そのものの実体はzend_eval_string
になります。
他にもエラーハンドリングなどもzend_XXX
などのphp-srcで定義されている関数群を呼んでいるだけです。
その他Buildパイプラインについて
WordPress Playground
と同様に依存関係を含めたDocker Imageを作ることで実現しました。
C言語レベルの開発が非常にしづらい構成になってはいるが、ローカルでの環境再現に時間がかかりそうだったので一旦同じ形にしました。
Reactの実装について
基本的には大きく2つの構成にしています。
- WebAssemblyにbuildしたPHPのラッパーパッケージ
-
Playground
本体
WebAssemblyにbuildしたPHPのラッパーパッケージについて
WordPress Playground
のフォークした内容をそのままベンダリングしています。
内容もそこまで難しいものはありません。
たとえばPHPのコードが実行するときに呼び出される箇所を見てみるとsetPHPCode
でWebAssembly側にコードをセットさせて、handleRequest
で実際に処理をすることがわかります。
setPHPCode
の内部的には前述したemscripten
のグルーコードを呼び出しているだけです。
WebAssemblyへのコンパイルの際に、コンパイラオプションを指定することでccall
というメソッド、WebAssembly上のC言語で定義された関数にアクセスできるようにしています。
handleRequest
は関数としては正常終了化判断するためにexitCode
をもらいつつ結果そのものは標準出力から受け取ります。
/tmp/stdout
、/tmp/stderr
に関しては事前にWebAssemblyが扱えるファイルシステム上に、WebAssembly上の/dev/stdout
, /dev/stderr
を該当パスにマウントしているために受け取れるようにしています。
扱えるファイルシステムに関してはemscriptenのFile System Overviewを御覧ください。デフォルトはオンメモリ(MEMFS
)です。
Playground本体について
今回、私自身がReact
初心者ということもあり、ほぼ素のReactで開発することにしました。
とはいえ以下の理由で追加のLibraryを入れてはいます。
- Chakura UI ... デザインの一貫性のため。
- React Router ... シェア機能実現のために状態をURLへの永続化・復元のために。
-
Sandpack Editor ... 薄いリアルタイムエディタだったので。
Chakura UI
のPlayground
がこれを使ってたので参考にもなりそうだったので。
また、状態管理もシンプルに以下の方針で行うことにしました。
- ローカルステートは
UseState
,UseEffect
- コンポーネント間の状態共有はPropsによるバケツリレー
ただこれだけだと再レンダリング時のハンドリングが煩雑だったので、 カスタムHookを作成しそこに状態管理を寄せた構成にはしました。
描画に関してはIframeのsrcdocにPHPの実行結果を挿入することで実現することにしました。
この理由に関してはsandbox属性の存在が大きく、簡単にXSSといった脆弱性の対策を実行できたためです。
出力結果のhtmlを取り除くことも考えましたが、phpinfo()
を動かしたときにhtml
で帰ってきた内容をそのまま描画するほうがカッコいいよな!!というところで今の実装に落ち着きました。
加えて、この実装だとPHPのテンプレートエンジンとしての動作検証が可能になります。
この点は他のPHPのPlayground系サービスとの差別化に繋がりました。
URLへの状態の永続化について
簡単にスニペットを実行結果含めて共有することを考えると、PlaygroundとしてはURLシェアによる状況再現が必要不可欠でしょう。
現在のPlaygroundに関しては大きく2つの状態があります。
- 実行中のPHPのバージョン
- 実行するPHPコード
これらをURLに埋め込み、復元することができることが求められました。
基本的には該当する状態の変更があった際にはReactRouterのusesearchparams
を使うことで解決をしました。
cには実行するPHPコード、vにはPHPのバージョンを埋め込みました。
- URLから状態を復元するとき
const [searchParams, setSearchParams] = useSearchParams();
const code = lzstring.decompressFromEncodedURIComponent(
searchParams.get('c') ?? ''
) ?? '<?php\n// example code\nphpinfo();';
const version = asVersion(searchParams.get('v')) ?? '8.2';
- URLに状態を埋め込むときはhookから受け取ったSetter関数を利用する。
setSearchParams(
{
v: version,
c: lzstring.compressToEncodedURIComponent(code)
})
ただし、PHPのコードの変更が起こった際は編集内容を1文字変更するたびにコンポーネントのレンダリングが発生し操作感に支障がでたので、
history.pushStateを利用することでURLの書き換えのみを行うことに留めました。
const state: UrlState = {
c: lzstring.compressToEncodedURIComponent(code),
v: version,
};
const urlSearchParam = new URLSearchParams(state).toString();
history.pushState(state, '', `?${urlSearchParam}`);
またPHPのプログラムコードをそのままURLのクエリパラメータに記載すると文字数が膨れ上がる問題があり、文字列圧縮を行う必要がありました。
そこでtypescript playground
を参考にlz-string
を利用しました。
日本語の解説に関しては、しなぷす様の下記の記事に詳細な解説が載っていますのでそちらをごらんください。
反省
公開後に気づいて少し後手に回った点が今回あったので、懺悔も兼ねて載せておきます。
私自身、今後ウェブサービス作るときは改めて気をつけておきたい点です。
URLの正規化
1枚のアプリケーションといえど、URLの正規化(canonicalの指定)は忘れずにしましょう。
あとsitemap.xmlも作っておくとindexはスムーズです。
今回の私の場合だと入力内容をURLに永続化させる機能が悪さをして、想定とは異なるページがindexされてしまいました。
執筆時点では再申請中です。
プライバシーポリシーについて
せっかく作ったので小遣い稼ぎを目指してアドセンスに依頼中です。執筆時点で審査には二度落ちています。
その際にプライバシーポリシーの作成は必須になります。忘れずに作りましょう。
自分はここで作りました。(海外込みでGDPRとか諸々つけたので私の場合は有料にはなりましたが、日本国内想定レベルなら無料で作れます)
連絡先の明記
これもアドセンス設置を目指したきっかけで気づいたものでした。
広告に関係なくても、運営者不明は怪しすぎてユーザーフレンドリーではないので、設置しましょう。
今回だとfooterに私のTwitterアカウント、リクエストフォームとしてIssueへのリンクを記載しました。
今後の予定
あとでReadmeにも載せておこうかなと思いますが、今後の開発ネタも記載しておきます。優先順位は気分で変わります。
改めて要望、報告はIssueへの起票ないしDMお待ちしています。
WebAssemblyのプロセスについて
現在はClientプロセスでPHPのWebAssemblyを動かしているため、中断といった割り込み処理なハンドリングが行えません。
現在だと無限ループするコードを入力してしまうとタブごと消すぐらいしか対処ができません。
Wordpress Playground
と同様にワーカによる別スレッド化する必要性があるなと考えています。
状態管理について
状態管理も改善の余地が大いにあるなと考えています。
Grobal State
は煩雑になりそうだったので扱わない方針だったが、
URLへの永続化および復元がGrobal State
そのものなので、ここは再設計の必要性があると感じています。
とくに、コード変更 -> URL再構成が結構シビアで、入力まわりを弄ると「無限ループ」や「入力時のDelayが発生する」といったバグないし体験悪化が起きやすくなっており、何かと開発を妨げる要因になってるので何とかしたいです。
ここに関しては、私自身のReact不慣れな要因は大いにあるなと感じているものの、業務でのReact導入の妨げになりそうなのでいい感じな方法を模索したいなと思っています。
一旦コード変更時のみHistory Apiを使うことで再構成させずに、URLの変更は行うようにはしました。
外部ライブラリの利用可能に
外部ライブラリ含めて検証できるようにしたいとは考えています。
言語レベルでの動作確認だけでも便利だとは思いますが、外部ライブラリの動作チェックもできるとなおいいかなあという気はしています。
リスペクトしているGo Playgroundは外部ライブラリ呼び出しが気軽にできるので、何とか頑張りたいところです。
まず、composerがPHP依存なので、そのままだと難しいという点が大いにあります。
そこで、バイナリで動く簡易版が作れたら、WebAssemblyへのbuild時に埋め込みとかできるようになるので取り組める可能性があります。
しかし.wasm
ファイルが肥大化して初期ロードの体験悪化に繋がりかねないといった点も生まれそうで、一筋縄にはいかないのでアイディア募集中です。
おわりに
荒削りながらも、わりと実用性のあるものができたのではないでしょうか。
ぜひPHPerのみなさん使ってくださいね。スターや拡散お待ちしています。
よかったらTwitterフォローもしてくれたら喜びます。
余談ですが、改めてPHPのコードを読んでいると、C言語が辛いという感想になりました。
というよりPHPのソースコードが私には難しかったです。.
マクロあると全然読めないです。なにかコードリーディングのコツとかあったら教えてください。
また、個人開発レベルでメンテされてなさそうですが、
Rustに移植しているものもあるので、いっそのことCから脱却するのはありかもしれないかなとは思いはしました。
一部PHPのソースにパッチを当てることは逃げられないので、完全には無理そうですが。
PHPに限った話ではないですが、過去にC/C++で作ったものをWebAssemblyにしてブラウザで動くようにするというのは一つのトレンドになりそうだと思いました。
これからが楽しみですね!!
Discussion