【入門編】PHPの内部関数(C API)におけるメモリ管理の注意点とリーク防止 – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

こんにちは。普段はPHPのフレームワークを使いこなし、「ビジネスロジックを書くのはお手のもの」というレベルに到達したエンジニアの多くが、ある日突然、拡張機能(Extension)の自作や、Zend C-APIの領域に足を踏み入れた途端に、得体の知れない「メモリの亡霊」に悩まされるようになります。

他の高水準言語(Node.jsやGoなど)であれば、ランタイムがよろしくやってくれるメモリ管理の世界から一歩踏み出し、C言語の地平へと降り立ったとき、私たちはZend Engineという強烈な黒幕のルールに従わなければなりません。

今回は、PHPの内部関数(C API)におけるメモリ管理の作法と、拡張開発者が最も陥りやすいメモリリークの罠について、エンジン内部の挙動を覗き見ながら一緒に紐解いていきましょう。ここを理解すれば、PHPの裏側で何が起きているのかが手に取るように見えてきますよ。

—

1. Zend Engineにおけるメモリ管理の基本思想

PHPのソースコード(C言語)を眺めると、お馴染みの `malloc` や `free` が直接使われている場面はほとんどありません。代わりに `emalloc` や `efree` といったマクロが登場します。

なぜZend Engineは、わざわざ独自のメモリマネージャ層を挟んでいるのでしょうか?

Webリクエストのライフサイクルと「一括解放(Request-bound Memory)」

PHPの最大の特徴であり強みは、「1リクエスト=完全に独立したプロセス(またはスレッド)の実行空間であり、リクエスト終了と共に全メモリが綺麗に消え去る」という、極めてプリミティブで安全なサンドボックス構造にあります。

Zend Engineのメモリマネージャ(ZMM: Zend Memory Manager)は、この「リクエスト単位のライフサイクル」に最適化されています。

  • `emalloc()` で確保されたメモリは、現在のリクエストコンテキストに紐づきます。
  • 仮に、拡張機能のコード内でうっかり `efree()` を忘れたとしても、HTTPリクエストが終了した瞬間(`request shutdown`)に、OSレベルではなくZMMが一網打尽にメモリを一括解放します。

「じゃあ、別に `efree` なんてサボってもリクエスト単位で見れば安全なんじゃ?」と思われましたか?
ここに、長く生きるデーモンプロセス(Swoole、RoadRunner、あるいはPHP-FPMの長寿命ワーカー)における最大の罠が潜んでいます。

—

2. 拡張機能開発における「静的・永続的メモリ」の罠

PHP-FPMなどの環境では、ワーカープロセスが何千ものリクエストを次々と処理します。もし、リクエスト間でデータを持ち回るような永続的な構造(`persistent allocation`)を扱う場合や、拡張機能内のグローバル変数などにメモリを割り当てる場合は、話が全く違ってきます。

ここで登場するのが、伝説的な関数群です。

| 通常のリクエスト内メモリ | 永続的(プロセス寿命)メモリ |
| :— | :— |
| `emalloc(size)` | `pemalloc(size, 1)` |
| `ecalloc(nmemb, size)` | `pecalloc(nmemb, size, 1)` |
| `erealloc(ptr, size)` | `perealloc(ptr, size, 1)` |
| `efree(ptr)` | `pfree(ptr, 1)` |

第2引数の `1`(`1 = persistent`)を指定すると、そのメモリはZendの通常のリクエストプールではなく、OSのヒープから直接(あるいはプロセス共通のプールから)確保されます。

恐ろしいメモリリークのシナリオ

もし、永続フラグ(`1`)を立てて確保したメモリを、リクエスト終了時に解放し忘れたらどうなるでしょうか?

// 悪い例:リクエスト毎に永続メモリを確保し、解放を忘れている拡張機能のコード片
PHP_FUNCTION(dangerous_memory_leak) {
// 第2引数に 1 を指定すると、リクエストが終了しても消えません!
char persistent_buffer = pemalloc(1024 1024, 1);

// 何らかの処理…

// pfree(&persistent_buffer, 1) が呼ばれないままリクエストが終了
RETURN_TRUE;
}

この関数が叩かれるたびに、PHP-FPMのワーカープロセスが抱えるメモリは1MBずつ肥大化していきます。リクエストが終わってもメモリは解放されないため、数千回のリクエストを処理した頃には、サーバーはOut of Memory (OOM) で無残にクラッシュすることになります。

永続メモリを扱うときは、「誰が、どのライフサイクルでそのメモリの寿命を終わらせるのか(`MSHUTDOWN` や `RSHUTDOWN` のタイミングなど)」を完全に設計図に組み込まなければなりません。

—

3. Zend Hash Table と zval の所有権問題

C APIでPHPの変数(`zval`)を扱う際、最も頭を悩ませるのが「参照カウント(Reference Counting)」とメモリの所有権です。

例えば、PHPの連想配列をC拡張側で操作する場合、内部の `HashTable` を操作します。

zval my_array;
array_init(&my_array);

// 文字列を配列に追加する
add_assoc_string(&my_array, “key”, “Zend Engine Internals”);

ここで作成された文字列 `”Zend Engine Internals”` は、内部でどのように管理されているでしょうか?
Zend Engineは、効率化のために文字列を複製(dup)して保持することがあります。もし、あなたが独自に `emalloc` で確保した文字列バッファを `add_assoc_string` に渡した場合、そのポインタの所有権は誰のものになるのでしょうか?

コピールールを誤ると二重解放(Double Free)が起きる

Zend APIの関数群には、大別して以下の2つのパターンが存在します。

1. 値をコピーして保持するパターン(例: `add_assoc_string` など)

  • 渡した文字列はエンジン側で複製されるため、呼び出し元で使った元のバッファは、呼び出し側責任で `efree` する必要があります。

2. ポインタの所有権をそのまま奪う(あるいは共有する)パターン(例: `add_assoc_zval` など)

  • `zval` の参照カウントをインクリメントしつつ、内部構造体に組み込みます。

ここを勘違いして、エンジンに渡したあとに自分で `efree` を呼んでしまうと、エンジン側がすでに解放されたメモリ領域を再度アクセスしようとし、Segmentation Fault(セグメンテーション違反)を引き起こしてプロセスが即死します。

—

4. 安全な拡張開発・メモリ管理のための実践的アプローチ

実務でPHPの内部やC拡張、あるいはFFI(Foreign Function Interface)などを通じてメモリを直接触る際、私たちが心得るべき鉄則を整理しておきましょう。

1. 原則として `emalloc` / `efree` のペアを同じスコープ内で完結させる

  • 確保した関数内で必ず解放する。例外処理(C言語にはPHPのような綺麗な try-catch が標準ではありません。`zend_error` や `zend_throw_exception` が走ると、通常の関数脱出ルートがバイパスされることがあります)を考慮し、ジャンプ先でも確実に解放される仕組み(あるいはリクエスト終了時のZMMの自動解放に頼れる設計)を意識します。

2. Valgrindを活用したメモリリーク・不正アクセスの検出

  • C拡張の開発では、必ず `valgrind` を使ってPHPを実行し、メモリリークや不正な読み書き(Invalid read/write)がないかを検証します。

valgrind –tool=memcheck –leak-check=full sapi/cli/php -r “your_extension_function();”

  • Zend Engineは独自のメモリプール(ZMM)を抱えているため、そのままではValgrindが標準の `malloc` 以外のリークを検知しにくい場合があります。そのときは環境変数 `ZEND_DONT_REALLOC_INTERNAL=1` や `USE_ZEND_ALLOC=0` を設定し、OSの標準アロケータを強制的に使わせることで、Valgrindの網にかかりやすくするテクニックが現場では常套手段として使われます。

—

最後に:低レイヤを知ることで、高水準なコードが美しくなる

ここまで、PHPの内部メモリ管理とC APIの作法についてディープに解説してきました。

普段私たちが何気なく書いている `$a = [1, 2, 3];` や、巨大な配列の処理、セッションのシリアライズといった操作も、すべてはこの厳格なZend Engineのメモリ管理の基盤の上で成り立っています。

「なぜこの書き方をするとメモリを圧迫するのか」「なぜこの場面でガベージコレクションが発動するのか」。その理由が低レイヤの視点からスッと腑に落ちたとき、あなたの書くPHPコードは、単に動くだけのコードから、マシンリソースを慈しむような美しく洗練されたシステムアーキテクチャへと昇華されます。

さあ、恐れずに裏側の世界を覗き、PHPを本当の意味で掌中に収めていきましょう。

タイトルとURLをコピーしました