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