summaryrefslogtreecommitdiffhomepage
path: root/vhosts/blog/content/posts/2023-10-02/compile-php-runtime-to-wasm.ndoc
blob: 19143c44d9936417d4aea298c0bad55fb419148c (plain)
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
---
[article]
uuid = "0ed1ccc8-d437-481c-8cca-2131ce800cc0"
title = "PHP の処理系を Emscripten で WebAssembly にコンパイルする"
description = "PHP の処理系 (php/php-src) を Emscripten で WebAssembly にコンパイルし、任意のコードを隔離された環境で評価できるようにした。"
tags = [
  "php",
  "wasm",
]

[[article.revisions]]
date = "2023-10-02"
remark = "公開"
---
<article>
  <section id="intro">
    <h>はじめに</h>
    <p>
      <a href="https://emscripten.org/">Emscripten</a> を用いて <a href="https://github.com/php/php-src">PHP の処理系</a>を <a href="https://developer.mozilla.org/docs/WebAssembly">WebAssembly</a> にコンパイルした。機能をある程度絞ることで、思ったよりも簡単に実現できたので、備忘録として記しておく。
    </p>
    <p>
      なお、この記事では Emscripten や WebAssembly とは何か知っていることを前提とする。
    </p>
  </section>
  <section id="version">
    <h>バージョン情報</h>
    <p>
      この記事中で使用するソフトウェア等のバージョンを記載する。
    </p>
    <ul>
      <li>Ubuntu 22.04 on WSL2</li>
      <li>Docker version 24.0.6</li>
      <li>Emscripten 3.1.46</li>
      <li>Node.js 20.7.0</li>
      <li>PHP 8.2.10</li>
    </ul>
    <p>
      なお、Docker から下は Docker 上で導入するので、ホストマシンにはインストールしなくてよい。
    </p>
  </section>
  <section id="goal">
    <h>本記事のゴール</h>
    <p>
      先にこの記事のゴールを示しておく。これから示す手順のとおりに進めると、次のようなコードが動くようになる。
      このコードはこのあと使うので、<code>index.mjs</code> の名前で保存しておくこと。
    </p>
    <codeblock language="javascript">
      <![CDATA[
      import { readFile } from 'node:fs/promises';
      import PHPWasm from './php-wasm.mjs'

      const code = await readFile('/dev/stdin', { encoding: 'utf-8' });

      const { ccall } = await PHPWasm();
      const result = ccall(
        'php_wasm_run',
        'number', ['string'],
        [code],
      );
      console.log(`exit code: ${result}`);
      ]]>
    </codeblock>
    <p>
      標準入力から与えたコードを WebAssembly にコンパイルされた PHP 処理系の上で実行している。このような <code>php-wasm.mjs</code> (とそこから呼び出される <code>php-wasm.wasm</code>) を作成する。
    </p>
  </section>
  <section id="build">
    <h>ビルド</h>
    <section id="write-c-entrypoint">
      <h>C のエントリポイントを書く</h>
      <p>
        先ほどのコードでも使っていたエントリポイントである <code>php_wasm_run</code> を用意する。
      </p>
      <codeblock language="c">
        <![CDATA[
        #include <stdio.h>
        #include <emscripten.h>
        #include <Zend/zend_execute.h>
        #include <sapi/embed/php_embed.h>

        int EMSCRIPTEN_KEEPALIVE php_wasm_run(const char* code) {
            zend_result result;

            int argc = 1;
            char* argv[] = { "php.wasm", NULL };

            PHP_EMBED_START_BLOCK(argc, argv);

            result = zend_eval_string_ex(code, NULL, "php.wasm code", 1);

            PHP_EMBED_END_BLOCK();

            fprintf(stdout, "\n");
            fflush(stdout);
            fprintf(stderr, "\n");
            fflush(stderr);

            return result == SUCCESS ? 0 : 1;
        }
        ]]>
      </codeblock>
      <p>
        ほとんどはただの PHP の公開 API を使ったコードだが、Emscripten 向けの注意点が 2点ある。
      </p>
      <p>
        まずは <code>EMSCRIPTEN_KEEPALIVE</code> について。
        これは Emscripten が用意している特殊なマクロである。
        このマクロが付与されている関数は、どこからも使用されていなくともコンパイル後の WebAssembly バイナリから削除されない。
        もしこれを付け忘れると、未使用の関数とみなされ削除される。
      </p>
      <p>
        次に、コードを評価したあとに呼んでいる標準出力と標準エラー出力に対する改行の出力について。
        出力バッファから出力させるためだけなら改行を出力させなくとも <code>fflush()</code> だけで事足りると考えたのだが、ないと動かなかったので追加した。
        これにより、PHP コードの出力の後ろに余分な改行が追加されてしまう。
        改行を出力せずともバッファを消費させる手段をご存知のかたはご教示願いたい。
      </p>
    </section>
    <section id="compile-to-wasm">
      <h>WebAssembly にコンパイルする</h>
      <p>
        それでは WebAssembly にコンパイルしていこう。ここからは <code>Dockerfile</code> 上のコマンドとして操作を示す。
      </p>
      <p>
        まずは <a href="https://hub.docker.com/r/emscripten/emsdk">Emscripten 公式が提供している Docker イメージ</a>を使って、PHP 処理系と先ほど示した C 言語のソースコードを WebAssembly にコンパイルする。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        FROM emscripten/emsdk:3.1.46 AS wasm-builder
        ]]>
      </codeblock>
      <p>
        次に、<a href="https://github.com/php/php-src">php/php-src</a> から PHP 処理系のソースコードを取得し、ビルドに必要な apt パッケージを取ってくる。
        有効にする拡張を増やしたいなら、ここでインストールするパッケージも増やすことになるだろう。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        RUN git clone --depth=1 --branch=php-8.2.10 https://github.com/php/php-src

        RUN apt-get update && \
            apt-get install -y --no-install-recommends \
                autoconf \
                bison \
                pkg-config \
                re2c \
                && \
            :
        ]]>
      </codeblock>
      <p>
        続けて、Emscripten のツールチェインを用いて PHP 処理系をビルドする。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        RUN cd php-src && \
            ./buildconf --force && \
            emconfigure ./configure \
                --disable-all \
                --disable-mbregex \
                --disable-fiber-asm \
                --disable-cli \
                --disable-cgi \
                --disable-phpdbg \
                --enable-embed=static \
                --enable-mbstring \
                --without-iconv \
                --without-libxml \
                --without-pcre-jit \
                --without-pdo-sqlite \
                --without-sqlite3 \
                && \
            EMCC_CFLAGS='-s ERROR_ON_UNDEFINED_SYMBOLS=0' emmake make -j$(nproc) && \
            mv libs/libphp.a .. && \
            make clean && \
            git clean -fd && \
            :
        ]]>
      </codeblock>
      <p>
        ここまでと比べると少し複雑なので、それぞれ詳しく見ていこう。
      </p>
      <p>
        まず、<code>buildconf</code> は PHP 処理系をビルドするときに (Emscripten とは関係なく) 使うツールである。
        このツールの最も重要な仕事は、<code>configure</code> の生成である。
      </p>
      <p>
        次に <code>configure</code> するわけだが、ここで <code>emconfigure</code> を使う。
        これを使うことで、Emscripten が上手く諸々のツールチェインを WebAssembly のビルド向けに調整しながら <code>configure</code> してくれる。
      </p>
      <p>
        <code>configure</code> の後ろに指定してあるフラグは、通常の PHP 処理系のビルドで使う <code>configure</code> と同じなので、詳しくはそちらの <code>cofigure --help</code> を参照していただきたい。
        ほとんどは、機能の無効化のために指定している (依存するライブラリを減らし、ビルドをより簡単にするため)。
      </p>
      <p>
        通常の C のビルドなら、<code>configure</code> の次は <code>make</code> するところだが、ここでも <code>emmake</code> を使う。
        役割はほとんど <code>emconfigure</code> と同様である。
        指定してある <code>EMCC_CFLAGS</code> という環境変数は、Emscripten の C コンパイラへのフラグで、ここでは <code>ERROR_ON_UNDEFINED_SYMBOLS</code> を無効化している。
        これにより、コンパイル中に出現した解決できなかったシンボルを無視するようになる (代わりに、そのシンボルを呼ぼうとしたタイミングで実行時エラーになる)。
        すべての依存を完全に解決するのは面倒なので、あまり使わない機能については無視してもよいだろう。
      </p>
      <p>
        ここまでを実行すると <code>libs/libphp.a</code> が生成される。これは後で使うので移動させている。
      </p>
      <p>
        さて、PHP 処理系をライブラリ化できたので、次に先ほど載せた C のソースコードをビルドしていこう。
        <code>Dockerfile</code> と同じ場所に <code>php-wasm.c</code> という名前で保存し、次のようにする。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        COPY php-wasm.c /src/

        RUN cd php-src && \
            emcc \
                -c \
                -o php-wasm.o \
                -I . \
                -I TSRM \
                -I Zend \
                -I main \
                ../php-wasm.c \
                && \
            mv php-wasm.o .. && \
            make clean && \
            git clean -fd && \
            :
        ]]>
      </codeblock>
      <p>
        <code>emcc</code> は <code>cc</code> (C コンパイラ/リンカ) の Emscripten 版で、<code>-c</code> は「コンパイル」の意。
        <code>-o</code> や <code>-I</code> は普通の C コンパイラと同様、出力ファイルの指定とインクルードパスの指定である。
      </p>
      <p>
        <code>libphp.a</code> と <code>php-wasm.o</code> が手に入ったので、これらをリンクして WebAssembly のバイナリとそのラッパである JavaScript ファイルを生成する。
        これにも <code>emcc</code> コマンドを使う。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        RUN emcc \
            -s ENVIRONMENT=node \
            -s ERROR_ON_UNDEFINED_SYMBOLS=0 \
            -s EXPORTED_RUNTIME_METHODS='["ccall"]' \
            -s EXPORT_ES6=1 \
            -s INITIAL_MEMORY=16777216 \
            -s INVOKE_RUN=0 \
            -s MODULARIZE=1 \
            -o php-wasm.js \
            php-wasm.o \
            libphp.a \
            ;
        ]]>
      </codeblock>
      <p>
        それぞれのフラグについて解説する。
      </p>
      <p>
        <code>-s ENVIRONMENT=node</code> は、生成する WebAssembly/JavaScript の実行環境を指定する。
        今回は <code>node</code> を指定しているので、Node.js 向けのファイルが生成される。
      </p>
      <p>
        <code>-s ERROR_ON_UNDEFINED_SYMBOLS=0</code> についてはすでに述べたので省略する。
      </p>
      <p>
        <code>-s EXPORTED_RUNTIME_METHODS='["ccall"]'</code> は、生成される JavaScript から公開される API である。
        すでに <code>index.mjs</code> で使用しているが、<code>ccall('関数名', '返り値の型', ['仮引数の型', ...], ['実引数', ...])</code> のように使う。
      </p>
      <p>
        <code>-s EXPORT_ES6=1</code> は、JavaScript コードを ECMAScript 6 に準拠した module として生成する。
        これを指定することで、<code>require()</code> ではなく <code>import</code> できる JavaScript を生成させられる。
      </p>
      <p>
        <code>-s INITIAL_MEMORY=16777216</code> は呼んで字のごとく。用途に合わせて適当に決めてほしい。
      </p>
      <p>
        <code>-s INVOKE_RUN=0</code> は、module をロードしたときに勝手に <code>main()</code> を呼ぶかどうか (だと思う)。
        今回は <code>php_wasm_run()</code> しか使うつもりがないので切っている。
      </p>
      <p>
        <code>-s MODULARIZE=1</code> は、実質的にほぼ必須のオプションであり、1 を指定することで「WebAssembly module をインスタンス化する関数」をエクスポートするような JavaScript ファイルを生成するようになる。
        これを指定しないと、生成物の JavaScript ファイルを読み込むと WebAssembly module が即座にインスタンス化されてしまい、起動のタイミングを制御できない。
      </p>
      <p>
        ここまで実行すると、<code>php-wasm.js</code> と <code>php-wasm.wasm</code> が作られる。
        では、ここからはこれらの実行環境を作っていこう。
      </p>
      <p>
        といっても、Node.js はビルトインで WebAssembly をサポートしているので、ほとんどやることはない。
        先ほど掲載した JavaScript のコードは、<code>Dockerfile</code> と同じディレクトリに <code>index.mjs</code> で配置すること。
      </p>
      <codeblock language="dockerfile">
        <![CDATA[
        FROM node:20.7

        WORKDIR /app
        COPY --from=wasm-builder /src/php-wasm.js /app/php-wasm.mjs
        COPY --from=wasm-builder /src/php-wasm.wasm /app/php-wasm.wasm
        COPY index.mjs /app/

        ENTRYPOINT ["node", "index.mjs"]
        ]]>
      </codeblock>
    </section>
  </section>
  <section id="run">
    <h>実行</h>
    <p>
      <code>Dockerfile</code>、<code>php-wasm.c</code>、<code>index.mjs</code> を用意したら、Docker コンテナをビルドして実行する。
    </p>
    <codeblock language="dockerfile">
      <![CDATA[
      $ docker build -t php-wasm .
      $ echo 'echo "Hello, World!", PHP_EOL;' | docker run --rm -i php-wasm
      Hello, World!


      exit code: 0
      ]]>
    </codeblock>
  </section>
  <section id="outro">
    <h>まとめ</h>
    <p>
      <a href="https://github.com/nsfisis/tiny-php.wasm">ここまでをまとめた Git リポジトリ</a>を用意した。
      簡単にコンパイルできるので、興味があれば試してみてほしい。
    </p>
  </section>
  <section id="references">
    <h>参考リンク</h>
    <ul>
      <li><a href="https://github.com/php/php-src">php/php-src: ビルドの方法について</a></li>
      <li><a href="https://emscripten.org/docs/getting_started/Tutorial.html">Emscripten: チュートリアル</a></li>
      <li><a href="https://emscripten.org/docs/compiling/Building-Projects.html#building-projects">Emscripten: ビルドの基本</a></li>
      <li><a href="https://emscripten.org/docs/tools_reference/emcc.html#emccdoc">Emscripten: <code>emcc</code> などのリファレンス</a></li>
      <li><a href="https://emscripten.org/docs/api_reference/module.html#module">Emscripten: 生成される JavaScript の API</a></li>
    </ul>
  </section>
</article>