🐘

はじめてのPHP(php-src)

に公開

概要

しばらく記事書いてなかったので、リハビリに軽い記事でも書こうかと。

最近PHPerKaigi2025があり、PHPで何かやりたい欲が出てきたので
PHPに関数生やして遊んでみようと思います。

前置き

この記事は、PHPの内部実装について解説する記事ではありません。
PHPに関数を雑に生やして遊ぶだけの記事です。
ちゃんと内部実装を知るには、php-srcのCONTRIBUTING.mdなどを見るのがいいと思います。

PHPの実装

PHPの実装はいくつかあるようですが、The PHP Groupによる公式実装がこちらです。
https://github.com/php/php-src

Go言語で出来た実装(frankenphp)もあります。
https://github.com/dunglas/frankenphp

ソースからビルドして動かしてみる

clone

とりあえず公式実装(php-src)のmasterをcloneします。
これを書いている時は、PHPのバージョンは8.5.0でした。

git clone -b master --depth 1 https://github.com/php/php-src.git

ビルド

Building PHP source codeUnix および macOS システムでのソースコードからのインストールを見ながらビルドします。

# Ubuntuの場合
sudo apt install -y \
  pkg-config build-essential autoconf bison re2c libxml2-dev libsqlite3-dev
./buildconf
./configure
make -j4

実行

PHPのバイナリは、sapi/cli/以下に出来ます。

sapi/cli/php --version
# PHP 8.5.0-dev (cli) (built: Mar 23 2025 01:29:32) (NTS)
# Copyright (c) The PHP Group
# Zend Engine v4.5.0-dev, Copyright (c) Zend Technologies

sapi/cli/php -r 'echo "hello, world!";'
# hello, world!

関数を追加してみる

PHPに新しい関数を生やしてみます。
N番目の素数を計算するAPI(calc_prime)を作ってみます。

PHPはZend Engineと呼ばれるインタプリタを内蔵しており
Zend Engineから引数を取得したり、Zend Engineに戻り値を返すことでPHPと連携できます。

ext/standard/basic_functions_arginfo.h

// 関数(calc_prime)を定義
ZEND_FUNCTION(calc_prime);

// 引数リストをarginfo_calc_primeという名前で定義
ZEND_BEGIN_ARG_INFO(arginfo_calc_prime, 0)
    ZEND_ARG_TYPE_INFO(0, n, IS_LONG, 0)
ZEND_END_ARG_INFO()

// 関数と引数リストを紐付け
static const zend_function_entry ext_functions[] = {
   //...
   ZEND_FE(calc_prime, arginfo_calc_prime)
   ZEND_FE_END   
};

ext/standard/basic_functions.c

// 整数Nを与えるとN番目の素数を計算して返却する関数
// 生成AIのPowerを少し借りた
ZEND_FUNCTION(hello_world)
{
    // Zend Engineを通して引数を取得
    zend_long target_index;
    ZEND_PARSE_PARAMETERS_START(1, 1)
        Z_PARAM_LONG(target_index)
    ZEND_PARSE_PARAMETERS_END();

    if (target_index <= 0) {
        RETURN_LONG(0);
    }

    if (target_index == 1) {
        RETURN_LONG(2);
    }

    unsigned int prime_limit = (unsigned int)(target_index * (log(target_index) + log(log(target_index)))) + 10;
    unsigned int square_root_limit = (unsigned int)sqrt(prime_limit) + 1;
    unsigned int prime_count = 1; // 2をカウント済み
    unsigned int latest_prime = 2;

    char *is_small_prime = malloc(square_root_limit);
    memset(is_small_prime, 1, square_root_limit);

    for (unsigned int candidate = 3; candidate * candidate < square_root_limit; candidate += 2) {
        if (is_small_prime[candidate]) {
            for (unsigned int multiple = candidate * candidate; multiple < square_root_limit; multiple += 2 * candidate) {
                is_small_prime[multiple] = 0;
            }
        }
    }

    unsigned int *prime_list = malloc(square_root_limit * sizeof(unsigned int));
    unsigned int small_prime_count = 0;
    prime_list[small_prime_count++] = 2;

    for (unsigned int candidate = 3; candidate < square_root_limit; candidate += 2) {
        if (is_small_prime[candidate]) {
            prime_list[small_prime_count++] = candidate;
        }
    }

    free(is_small_prime);

    const unsigned int SEGMENT_SIZE = 1 << 15;
    unsigned char *segment_flags = malloc(SEGMENT_SIZE);

    for (unsigned int segment_start = 3; segment_start <= prime_limit; segment_start += SEGMENT_SIZE) {
        unsigned int segment_end = segment_start + SEGMENT_SIZE - 1;
        if (segment_end > prime_limit) {
            segment_end = prime_limit;
        }

        memset(segment_flags, 1, segment_end - segment_start + 1);

        for (unsigned int prime_index = 0; prime_index < small_prime_count; prime_index++) {
            unsigned int small_prime = prime_list[prime_index];
            unsigned int first_multiple = small_prime * small_prime;

            if (first_multiple < segment_start) {
                first_multiple = (segment_start + small_prime - 1) / small_prime * small_prime;
            }

            for (unsigned int composite = first_multiple; composite <= segment_end; composite += small_prime) {
                segment_flags[composite - segment_start] = 0;
            }
        }

        for (unsigned int offset = 0; offset <= segment_end - segment_start; offset++) {
            if (segment_flags[offset]) {
                latest_prime = segment_start + offset;
                prime_count++;
                if (prime_count == target_index) {
                    free(segment_flags);
                    free(prime_list);
                    RETURN_LONG(latest_prime);
                }
            }
        }
    }

    free(segment_flags);
    free(prime_list);
    RETURN_FALSE;
}

実行結果

time sapi/cli/php -r 'echo calc_prime(10000000);'                                       355ms   Tue Mar 25 02:05:46 2025
179424673
________________________________________________________
Executed in  359.71 millis    fish           external
   usr time  350.11 millis    0.14 millis  349.98 millis
   sys time    7.16 millis    1.07 millis    6.09 millis

10000000番目の素数が約360msで算出できたよ!
ちなみに以下のスペックの私用PC(MBP)を使用しました。

所感

想定していたより簡単に関数生やせたなという印象です。
Zend Engineによる構文解析部分と分離されているおかげだと思います。
ただ、php-srcのビルドは最初手間取りました。(主にautotools関連の依存で)

また、php-srcはgitの履歴を見る限り26年選手で、いくつか気になる点もありました。
巨大リポジトリで依存も多いので、変えるのは非常に難しそうですが...

  • cmakeやmeson移行してNinja Build出来たら速そう(しかし労力)
  • フォーマッタ欲しい(git-clang-formatで差分だけかかるようにしたらいいかも?)
  • オブジェクトファイルはソースとは別ディレクトリに出来たら嬉しい
  • etc...

まとめ

PHPはいいぞ(雑)

Discussion