📘

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

2023/03/20に公開

はじめに

こんにちは @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 にしてブラウザで動くようにするというのは 1 つのトレンドになりそうだと思いました。
これからが楽しみですね!!

Discussion