Compose V2時代に複数PHPバージョンのエクステンション開発
この記事はリンケージ×PR TIMES合同勉強会の発表内容を文章にしたものです。
背景
PHPerKaigi 2023 で発表しました PHPの配列とデータ構造 に関して情報を集めていくに当たり、 PHP の配列は PHP ≦ 8.1 と PHP ≧ 8.2 で実装が異なることに気づきました。
PHP ≧ 8.2 からの新しい実装は、「Use more compact representation for packed arrays.」という Pull Request で議論・実装されており、PHP 7 以降で取り入れられた Packed Arrays (連続した整数添字の配列)[1][2]のメモリ空間の効率化するものでした。
この変更はソースコードを読めば自明ですが、実際に動いている PHP の HashTable の中身を見たいという願望が強くなったので、 HashTable に直接アクセスして内部の状態を出力するような PHP Extension を開発することにしました。[3]
1. プロジェクト構成や前提
開発環境
- Docker Desktop for Mac
- Compose V2
- (PhpStorm)
※ PhpStorm は docker compose コマンドを GUI で実行する時の説明で使います。 CLI でもやっていることは同じなので無くても問題ないです。
プロジェクト構成
/something-dir
├── php-src
│ └── ...
└── this-project
├── Dockerfile
├── compose.yml
├── ext
│ └── my_ext
└── src
└── hello.php
プロジェクト自体はthis-project以下になります。 my_ext が自作の PHP Extension になります。
今回は 1 つの Dockerfile で複数バージョンの PHP を扱います。
作成した Extension を動かす PHP コードはhello.phpになります。
php-src は PHP の本体である php/php-src です。これは、 PHP Extension Skeleton を作成する為の ext_skel.php を実行するために用意しています。
2. PHP Extension の作成と動作確認
手始めに、 Docker ではなくローカルマシンで動くことを確認してみます。
PHP Extension の雛形作成
$ cd /something-dir/php-src/ext
$ php ext_skel.php --ext my_ext
/something-dir/php-src/ext/my_extが生成されます。これが雛形になります。
php-src は PHP-8.2 にしています。 PHP-8.3 からはちょっとわからないですが、 PHP-8.x (≦8.2)であれば ext_skel.php で生成したコードを使い回せるかと思います。
ビルド
$ cd my_ext
$ phpize
$ ./configure --enable-my_ext
$ make
make まで行うと、modules/というディレクトリが出来ます。modules/にはmy_ext.soが生成されているかと思います。
では、この Extension を使って PHP を実行してみます。
$ php -d extension=modules/my_ext.so wip_hello.php
The extension my_ext is loaded and working!
<?php
test1();
Extension を改造してみる
作成した Extention は my_ext.c というファイルに実装が書かれています。
雛形で作成されている test2 を書き換えてみます。
PHP_FUNCTION(test2)
{
char *var = "World";
size_t var_len = sizeof("World") - 1;
zend_string *retval;
ZEND_PARSE_PARAMETERS_START(0, 1)
Z_PARAM_OPTIONAL
Z_PARAM_STRING(var, var_len)
ZEND_PARSE_PARAMETERS_END();
+ retval = strpprintf(0, "Hello PHP %s", var);
- retval = strpprintf(0, "Hello %s", var);
RETURN_STR(retval);
}
再びビルドして実行してみます。
$ phpize
$ ./configure --enable-my_ext
$ make
$ php -d extension=modules/my_ext.so wip_hello.php
Hello PHP World
<?php
echo test2() . "\n";
3. Docker で動かす
無事 PHP Extention が動くことを確認できたので、次は Docker で動かしてみます。
使用するイメージは Official Image の 8.2-cli です。
docker build で PHP Extention も build する
すでに PHP Extention を build しましたが、そのままビルドした Extention を Docker 環境(linux環境)では使うことは出来ません。
なので、必要ファイルを Docker 環境内に COPY して Docker 環境内で build します。
PHP の Official Image には docker-php-ext-install というコマンドが用意されています。
docker-php-ext-installを使うように Dockerfile を書きます(この挙動については後ほど説明します)
FROM php:8.2-cli
COPY ext/my_ext /usr/src/php/ext/my_ext
RUN docker-php-ext-install my_ext
Docker compose を使うので compose.yml は以下のように書きます。
services:
php-8.2:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
docker compose up -d --buildを行い、イメージのビルドと共に PHP Extention の build も行います。
PhpStorm で立ち上げる
Docker ツール設定から Use Compose V2 にチェックを入れます。これで Docker Compose を使用する時に Compose V2 が選択されるようになります。

次にcompose.ymlファイルの横にある実行マークをポチッと押します。

サービスツールウィンドウを見て、実行中になっている事を確認します。

右側にTerminalというボタンがあるので、これを押して Terminal を起動します。

ここで hello.php を実行すると、 PHP Extention が読み込まれた PHP を実行できます。

docker-php-ext-install は何をしているか
Dockerfile 内で docker-php-ext-install を呼び出していましたが、この挙動を確認してみます。
RUN docker-php-ext-install my_ext
このコマンドは cd /usr/src/php/ext をして、指定された Extension 名と同じ名前のディレクトを探します。
なので、 PHP Extension のディレクトリごと /usr/src/php/ext/ 配下にコピーします。
COPY ext/my_ext /usr/src/php/ext/my_ext
次に、この Extension に移動して、 docker-php-ext-configure を呼び出します。これは phpize と ./configure を実行します。
最後に make を実行して、 docker-php-ext-enable を呼び出します。これで、実行中の PHP に自作 Extension が使える状態になりました。
4. 複数バージョンに対応する
Docker で PHP Extension を動かすことが出来るようになったので、続いては複数のバージョンに対応してみます。今回は 8.2 に加え、 8.1 に対応してみます。
services:
php-8.1:
build:
context: .
dockerfile: Dockerfile
args:
PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
php-8.2:
build:
context: .
dockerfile: Dockerfile
args:
PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
ARG を使う
Docker のイメージに引数を指定できる ARG というものがあります。これを使っていきます。
まずは、 Dockerfile に ARG を加え、 PHP_VERSION という引数をつくります。これを FROM で指定するバージョンを指定できます。
+ ARG PHP_VERSION
+ FROM php:${PHP_VERSION}
- FROM php:8.2-cli
COPY ext/my_ext /usr/src/php/ext/my_ext
RUN docker-php-ext-install my_ext
compose.ymlには、それぞれが指定するバージョンを引き渡すように設定します。
services:
php-8.1:
build:
context: .
dockerfile: Dockerfile
+ args:
+ PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
php-8.2:
build:
context: .
dockerfile: Dockerfile
+ args:
+ PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
これで複数のバージョンの PHP のコンテナを立ち上げられました。

ここで先程同様に Terminal を開いて、 PHP を実行すると、自作 PHP Extension がインストールされた状態になっています。
(PHP ≦ 7.x について)
EOL ですね。
いや、 EOL ですが、どうしても必要っていう場合ありますね。わかります。
(これは当然ですが、) PHP はバージョンアップすると Zend API も追加・変更・削除されます。なので、メジャーバージョンを全くと難易度が上がります。
やり方としては、 Extension の雛形を作成する ext_skel.php を呼び出す php-src のローカルブランチを PHP-7.4 とか PHP-5.6 とかに切り替えて、 PHP Extension を作成し、 #if (PHP_VERSION_ID < 80000) のようにマクロを使って呼び出す API を切り替えると良いかなと思います。
※あくまで、個人の見解です。
PHP 8.2 の配列を見る PHP Extension
PHP Extension の実装
さて、ここから先はタイトルから少し外れますが、背景で話していた PHP ≧ 8.2 で配列の内部実装が異なることを PHP Extension を使って確認してみます。
配列を受け取って HashTable の情報をプリントする array_info という関数を作ってみます。
PHP_FUNCTION(array_info)
{
zval *arr;
// $a という引数を取り、配列であることを期待する
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY(arr)
ZEND_PARSE_PARAMETERS_END();
php_printf("ht->nTableMask: %#x\r\n", Z_ARRVAL_P(arr)->nTableMask);
php_printf("ht->nNumUsed: %d\r\n", Z_ARRVAL_P(arr)->nNumUsed);
php_printf("ht->nTableSize: %d\r\n", Z_ARRVAL_P(arr)->nTableSize);
php_printf("ht->nNumOfElements: %d\r\n", Z_ARRVAL_P(arr)->nNumOfElements);
php_printf("ht->nNextFreeElement: %d\r\n", Z_ARRVAL_P(arr)->nNextFreeElement);
php_printf("ht->nInternalPointer: %d\r\n", Z_ARRVAL_P(arr)->nInternalPointer);
php_printf("-------------\r\n");
#if (PHP_VERSION_ID >= 80200)
php_printf("HT_PACKED_SIZE: %d\r\n", HT_PACKED_SIZE(Z_ARRVAL_P(arr)));
php_printf("HT_PACKED_USED_SIZE: %d\r\n", HT_PACKED_USED_SIZE(Z_ARRVAL_P(arr)));
#else
php_printf("HT_SIZE: %d\r\n", HT_SIZE(Z_ARRVAL_P(arr)));
php_printf("HT_USED_SIZE: %d\r\n", HT_USED_SIZE(Z_ARRVAL_P(arr)));
#endif
}
引数や返り値については C のコード内でも指定していますが、 my_ext.stab.php というファイルにも記述します。
+function array_info(array $a): void {}
準備が出来ました。 docker compose up します。
実行
実行するファイルは以下の通りです。配列は Packed Array になるようにします。
<?php
echo PHP_VERSION . "\n";
$a = [1, 2, 3, 4];
array_info($a);
実行結果です。


期待通り、 PHP 8.2 では Packed Array の使用メモリが少なくなっていることがわかりました。
(その他)余談
最近、コロプラさんが開発されている PHP Extension を OSS として公開されました。
このリポジトリの実行されている Actions を見ると感動します。
まとめ
Dockerfile で ARG を設定する、作成された EXtension はディレクトリごと /usr/src/php/ext/[extension] にコピー、 docker-php-ext-install を実行。
ARG PHP_VERSION
FROM php:${PHP_VERSION}
COPY ext/my_ext /usr/src/php/ext/my_ext
RUN docker-php-ext-install my_ext
PHP のバージョンは compose.yml の args で指定する。
services:
php-8.1:
build:
context: .
dockerfile: Dockerfile
args:
PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
php-8.2:
build:
context: .
dockerfile: Dockerfile
args:
PHP_VERSION: 8.1-cli
volumes:
- ./src:/workspace
working_dir: /workspace
tty: true
参考文献
-
実際には必ずしも整数が連続している必要は無く、テーブルサイズを下回っていれば Packed Array となる ↩︎
-
「それって gdb で print_ht すれば良いのでは?」「わかる」 ↩︎
Discussion