📘

WebAssemblyを使ったPHPのリアルタイムPlaygroundを作りました。

2023/03/20に公開約10,700字

はじめに

こんにちは @glassmonekeyです。
久しぶりの投稿になります。
今回は個人開発ネタでWebAssemblyを使ったPHPのPlaygroundを作ったので紹介になります。

PHPをWebAssemblyで動かす仕組みそのものに関しては
2023年3月23日にて
PHPerKaigi2023PHPをブラウザで動かす技術も話す予定なので
そちらも合わせてご確認ください。この記事ではWebAssemblyそのものやPHPのBuildについてそこまで触れません。

https://fortee.jp/phperkaigi-2023/proposal/de0c3936-8780-487b-a9ce-92a8b90da480

この記事では主にサービスに関する実現手段にフォーカスしての紹介になります。

作ったもの

https://php-play.dev

demo

ソースコードはこちらになります。

なぜ作ろうと思ったのか

PHPで簡単なスニペットの実行と共有に課題を感じていたためです。
コードレビューといったコミュニケーションの場面や、文法の確認といった様々なスニペットの実行や共有へのニーズはあるのではないでしょうか。

私自身、普段はPHP以外にもGoを書くことがそこそこありまして、Goの場合だとGo Playground
でスニペットの実行と共有は簡単に行うことができます。

Go Playground

その点、PHPはこれといった決定的なPlaygroundはなかったように感じます。

そこで、今回は以下の点にこだわって作ってみることにしました。

  • リアルタイム性 ... 記述した内容がその場で描画されるように。
  • シェアのしやすさ ... 編集した内容をURLに埋め込みし、URLから入力内容を再現できるように。
  • PHPらしさ ... 本来PHPはHTMLのテンプレートエンジンの側面があったはずなので、HTMLもレンダリング可能なように。

特にリアルタイム性の観点からWebAssemblyを使うことにしました。

先行プロダクト

今回開発するにあたって、PHP + Assemblyが一番の困難な点だったので、 参考にさせていただいた先行プロジェクトを紹介しておきます。

oraoto/pib

おそらく元祖PHP + Assemblyのプロジェクトです。デモとしてPlaygroundが用意されています。

oraoto/pibによるPlayground

https://github.com/oraoto/pib

このプロジェクトの存在を認知していたことが、今回作ったきっかけの大きな要因でした。

ただビルド環境を再現することに再現することが難しく、PHPの対応バージョンも7.4のみだったりと参考にするには課題が山盛りでした。
そのため、このプロジェクトはそこまで参考にせず後述するWordpress Playgroundの方をベースに取り込みました。
とは言っても、Wasmのビルド方法の大半はoraoto/pibからの源流を組んでいるものが多く、その大半が以下のPR起因なので詳細は不明な点が多かったりはします。
(良い子のみんなはNo descriptionはやめようね!!)

https://github.com/oraoto/pib/pull/52

ちなみに認知したきっかけそのものは去年の PHP Conference Japan 2022で登壇した際の資料調査で見つけたことがきっかけだったりはします。
よかったらこれも見てくれると喜びます。

WordPress Playground

みなさんご存知WordPressPlayground版でなんとブラウザで動きます。

WordPress Playgroundのデモ

https://developer.wordpress.org/playground/

ソースコードはこちら

ドキュメント周り充実しているので、一読してみるとWebAssemblyそのものの入門にいいかもしれません。
https://wordpress.github.io/wordpress-playground/pages/index.html

最近ビルドプロセスの改善が行われており、近内にWasmにbuildしたPHPを呼び出すためのnpmパッケージが公開されるようです。
https://github.com/WordPress/wordpress-playground/pull/151

ちなみにこの記事を執筆していた時点ではビルドプロセス部分をフォーク&ベンダリングして取り込むことをしました。
npmパッケージが公開されたらそれに乗り換える可能性は高いかなと思います。

実装詳細について

それでは簡単にPlaygroudそのもの実装部分の解説をします。

SPA予定だったのと、私自身Reactの経験が乏しかったのでReact + TypeScriptで実装をしました。
また、扱うコンテンツが静的コンテンツのみなのでGit hubPagesにしました。
WebAssemblyのbuildについてと、BuildしたWebAssemblyを呼び出すReactアプリケーションについてそれぞれ説明します。

WebAssemblyのBuildについて

PHPをWebAssembly用にbuildもすこし触れてはおきます。

WebAssemblyへのbuild方法

PHPそのものはC言語なので、 コンパイラーにはemscriptenを用います。

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で定義されている関数群を呼んでいるだけです。

https://github.com/glassmonkey/php-playground/blob/4aec4971c9565e56bdc605e56ff94424c1f68156/src/wasm/build-assets/php_wasm.c#L1282-L1300

その他Buildパイプラインについて

WordPress Playgroundと同様に依存関係を含めたDocker Imageを作ることで実現しました。
C言語レベルの開発が非常にしづらい構成になってはいるが、ローカルでの環境再現に時間がかかりそうだったので一旦同じ形にしました。

https://github.com/glassmonkey/php-playground/tree/master/src/wasm

Reactの実装について

基本的には大きく2つの構成にしています。

  • WebAssemblyにbuildしたPHPのラッパーパッケージ
  • Playground本体

WebAssemblyにbuildしたPHPのラッパーパッケージについて

WordPress Playgroundのフォークした内容をそのままベンダリングしています。
内容もそこまで難しいものはありません。

たとえばPHPのコードが実行するときに呼び出される箇所を見てみるとsetPHPCodeでWebAssembly側にコードをセットさせて、handleRequestで実際に処理をすることがわかります。

https://github.com/glassmonkey/php-playground/blob/4aec4971c9565e56bdc605e56ff94424c1f68156/src/php-wasm/php.ts#L329-L355

setPHPCodeの内部的には前述したemscriptenのグルーコードを呼び出しているだけです。
WebAssemblyへのコンパイルの際に、コンパイラオプションを指定することでccallというメソッド、WebAssembly上のC言語で定義された関数にアクセスできるようにしています。

https://github.com/glassmonkey/php-playground/blob/4aec4971c9565e56bdc605e56ff94424c1f68156/src/php-wasm/php.ts#L540-L543

handleRequestは関数としては正常終了化判断するためにexitCodeをもらいつつ結果そのものは標準出力から受け取ります。

https://github.com/glassmonkey/php-playground/blob/4aec4971c9565e56bdc605e56ff94424c1f68156/src/php-wasm/php.ts#L544-L558

/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 UIPlaygroundがこれを使ってたので参考にもなりそうだったので。

また、状態管理もシンプルに以下の方針で行うことにしました。

  • ローカルステートはUseState, UseEffect
  • コンポーネント間の状態共有はPropsによるバケツリレー

ただこれだけだと再レンダリング時のハンドリングが煩雑だったので、 カスタムHookを作成しそこに状態管理を寄せた構成にはしました。
https://github.com/glassmonkey/php-playground/blob/master/src/app.tsx#L47-L96

描画に関してはIframeのsrcdocにPHPの実行結果を挿入することで実現することにしました。
この理由に関してはsandbox属性の存在が大きく、簡単にXSSといった脆弱性の対策を実行できたためです。
出力結果のhtmlを取り除くことも考えましたが、phpinfo()を動かしたときにhtmlで帰ってきた内容をそのまま描画するほうがカッコいいよな!!というところで今の実装に落ち着きました。

加えて、この実装だとPHPのテンプレートエンジンとしての動作検証が可能になります。
この点は他のPHPのPlayground系サービスとの差別化に繋がりました。

phpinfo

URLへの状態の永続化について

簡単にスニペットを実行結果含めて共有することを考えると、PlaygroundとしてはURLシェアによる状況再現が必要不可欠でしょう。
現在のPlaygroundに関しては大きく2つの状態があります。

  • 実行中のPHPのバージョン
  • 実行するPHPコード

これらをURLに埋め込み、復元することができることが求められました。
基本的には該当する状態の変更があった際にはReactRouterのusesearchparamsを使うことで解決をしました。
https://reactrouter.com/en/main/hooks/use-search-params

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}`);

https://developer.mozilla.org/ja/docs/Web/API/History/pushState

またPHPのプログラムコードをそのままURLのクエリパラメータに記載すると文字数が膨れ上がる問題があり、文字列圧縮を行う必要がありました。

そこでtypescript playgroundを参考にlz-stringを利用しました。

https://github.com/pieroxy/lz-string

日本語の解説に関しては、しなぷす様の下記の記事に詳細な解説が載っていますのでそちらをごらんください。

https://qiita.com/h164tan1/items/1cb93ebe5f1f97566858

反省

公開後に気づいて少し後手に回った点が今回あったので、懺悔も兼ねて載せておきます。
私自身、今後ウェブサービス作るときは改めて気をつけておきたい点です。

URLの正規化

1枚のアプリケーションといえど、URLの正規化(canonicalの指定)は忘れずにしましょう。
あとsitemap.xmlも作っておくとindexはスムーズです。
今回の私の場合だと入力内容をURLに永続化させる機能が悪さをして、想定とは異なるページがindexされてしまいました。
執筆時点では再申請中です。

プライバシーポリシーについて

せっかく作ったので小遣い稼ぎを目指してアドセンスに依頼中です。執筆時点で審査には二度落ちています。
その際にプライバシーポリシーの作成は必須になります。忘れずに作りましょう。
自分はここで作りました。(海外込みでGDPRとか諸々つけたので私の場合は有料にはなりましたが、日本国内想定レベルなら無料で作れます)
https://www.freeprivacypolicy.com/

連絡先の明記

これもアドセンス設置を目指したきっかけで気づいたものでした。

広告に関係なくても、運営者不明は怪しすぎてユーザーフレンドリーではないので、設置しましょう。
今回だと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のみなさん使ってくださいね。スターや拡散お待ちしています。
https://github.com/glassmonkey/php-playground

よかったらTwitterフォローもしてくれたら喜びます。
https://twitter.com/glassmonekey

余談ですが、改めてPHPのコードを読んでいると、C言語が辛いという感想になりました。
というよりPHPのソースコードが私には難しかったです。.
マクロあると全然読めないです。なにかコードリーディングのコツとかあったら教えてください。

また、個人開発レベルでメンテされてなさそうですが、
Rustに移植しているものもあるので、いっそのことCから脱却するのはありかもしれないかなとは思いはしました。
一部PHPのソースにパッチを当てることは逃げられないので、完全には無理そうですが。
https://docs.rs/php-all-sys/latest/php_all_sys/index.html

PHPに限った話ではないですが、過去にC/C++で作ったものをWebAssemblyにしてブラウザで動くようにするというのは一つのトレンドになりそうだと思いました。
これからが楽しみですね!!

Discussion

ログインするとコメントできます