【実務・中級編】PHP C APIにおける`efree()`と`zval_ptr_dtor()`の厳密な使い分けとメモリリーク防止:拡張開発者向け実践ガイド – PHPコア・内部エンジンと高速化・並行処理の極意解析バイブル

PHPコア・内部エンジンと高速化・並行処理の極意

PHP C APIにおける`efree()`と`zval_ptr_dtor()`の厳密な使い分けとメモリリーク防止:拡張開発者向け実践ガイド

テックリードの私たちがコードレビューで最も冷や汗をかく瞬間、それはPHPのC拡張モジュール(Extension)のプルリクエストを見たときだ。Zend Engineのメモリ管理モデルを理解せず、ただ「コンパイルが通るから」「セグメンテーションフォールfault(セグフォ)が起きていないから」という理由でマージされたコードは、本番環境のトラフィックの荒波にもまれ、突発的なメモリリークや、二重解放(Double Free)による致命的なプロセスダウンを引き起こす。

PHPは動的言語であり、開発者はメモリ管理の苦しみから解放されている。しかし、ひとたびC言語で拡張を書く、あるいはZend C APIの境界に踏み込むとなれば話は別だ。リクエストのライフサイクルが終わればすべてが消え去るという甘い幻想は捨てなければならない。

今回は、拡張開発者が必ず直面する最大の罠——`efree()` と `zval_ptr_dtor()` の厳密な使い分けについて、Zend VMの内部構造(Zend Memory Managerと参照カウント)の低レイヤから徹底的に解剖する。

—

1. Zend Engineにおけるメモリ管理の二大世界

PHPの内部(Zend Engine)において、メモリの割り当てと解放には明確な「文脈」が存在する。ここを混同することが、すべてのメモリリークの始まりだ。

Zend Memory Manager (ZMM) と `emalloc / efree`

PHPスクリプトの実行中、変数の値や内部構造体は、OSから直接`malloc`するのではなく、Zend Memory Manager(ZMM)を介してプールから割り当てられる。

  • `emalloc(size)`: リクエストスコープのメモリを確保する。
  • `efree(ptr)`: `emalloc`で確保されたメモリを即座に解放する。

重要なのは、`efree()` はただのメモリ解放関数であり、中身のデータ構造や参照カウントのことは一切考慮しないという点だ。もし、中に別のZendコンテナ(`zval`や配列、オブジェクトなど)が内包されていた場合、それらを再帰的にクリーンアップせずに `efree()` を叩けば、内部のポインタは宙に浮き、確実にメモリリーク(あるいは不正アクセス)を引き起こす。

zval と 参照カウント(Reference Counting)

PHPの変数の実体である `zval`(Zend Value)構造体は、8バイトの型情報と値(またはポインタ)を持ち、複雑なデータ型(文字列、配列、オブジェクト、リソース)の場合はヒープ上のメモリを指し示す。
Zend Engineは、メモリの効率的な共有とコピーオンライト(Copy-on-Write)を実現するため、すべてのヒープ割り当て型 `zval` のヘッダに `refcount`(参照カウント)を持たせている。

  • `Z_ADDREF_P(zval)`: 参照カウントをインクリメントする。
  • `zval_ptr_dtor(zval)`: 参照カウントをデクリメントし、カウントが0に達した時点で自動的にメモリを解放する(ガベージコレクションのトリガー)。

—

2. `efree()` と `zval_ptr_dtor()` の決定的な違い

コードレビューでよくある誤りが、「`zval`を片付けたいから、とりあえず `efree()` を使っておこう」という安易なアプローチだ。

| 特性 | `efree(void ptr)` | `zval_ptr_dtor(zval zv)` |
| :— | :— | :— |
| 対象 | 生のメモリブロック(文字列バッファ、独自構造体など) | `zval` コンテナおよびその配下のデータ構造 |
| 参照カウントの操作 | 一切しない | `refcount` をデクリメントし、0なら解放 |
| 再帰的解放 | しない(フラットなメモリのみ) | する(配列の要素やオブジェクトプロパティも連鎖解放) |
| 主な用途 | 拡張独自のバッファ、`estrndup`で複製した文字列の破棄 | PHPスクリプト側から渡された、あるいは内部生成した `zval` の後始末 |

⚠️ 危険なアンチパターン:`zval` に対して `efree()` を使う

もし `zval`(特に文字列や配列へのポインタを持つもの)に対して直接 `efree()` を呼び出すとどうなるか?
1. `zval` 自体が占有していたメモリ領域は即座に解放される。
2. しかし、その `zval` が内部で保持しているヒープ上の文字列やハッシュテーブル(HashTable)へのポインタは、解放されずにそのまま残る。
3. 結果として、ポインタの参照先を失った孤児メモリ(Orphaned Memory)が生まれ、FPMプロセスのメモリフットプリントがリクエストごとに肥大化していく。これが「静かなるメモリリーク」の正体だ。

—

3. 実践:安全な拡張コードとメモリリーク防止の設計パターン

では、C拡張開発やFFI、あるいはPHPコアの挙動を模した高度な処理において、どのようにメモリを制御すべきか。具体的なC APIのコード例(擬似コード含むC言語拡張の文脈)を通じて、堅牢な設計ルールを確認する。

以下のコードは、拡張内で連想配列(HashTable)を受け取り、その中の特定の要素を安全に操作・破棄する典型的なシーンを想定している。

zend_api_sample.c
include “php.h”

/

  • 堅牢なzval処理のサンプル関数
  • 渡された配列から値を取り出し、適切に参照カウントを管理しながら破棄する

/
PHP_FUNCTION(safe_zval_cleanup_demo)
zval array_arg;
zval value;
zend_string key;

// PHP側から配列を受け取る
if (zend_parse_parameters(ZEND_NUM_ARGS(), “a”, &array_arg) == FAILURE) {
RETURN_THINGS_ERROR(); // 実際のマクロやエラーハンドリングに置き換え
}

// ハッシュテーブルの走査
ZEND_HASH_FOREACH_STR_KEY_VAL(Z_ARRVAL_P(array_arg), key, value) {

// 値を一時的に安全に保持するため、参照カウントをインクリメントする
Z_TRY_ADDREF_P(value);

// — 何らかの重い処理 —
php_printf(“Processing item…\n”);

// 処理が終わったので、インクリメントした分の参照をデクリメントする
// ここで efree() を使ってはいけない!
zval_ptr_dtor(value);

} ZEND_HASH_FOREACH_END();

// 独自に確保した生メモリバッファの例
char custom_buffer = emalloc(1024);
// … バッファを使った処理 …

// 生のメモリブロックなので、ここでは正確に efree() を使う
efree(custom_buffer);

RETURN_TRUE;
}

この設計が美しい理由

1. `zval` には一貫して `zval_ptr_dtor()` を使用している: 配列の要素やPHP変数の実体である `zval` は、Zend Engineのライフサイクル管理に委ねるべきだ。参照カウントの整合性を崩さないため、直接 `efree()` で切り刻むような真似は絶対にしない。
2. 生バッファには `emalloc / efree` を明示的に適用している: PHPの変数体系に載せない、拡張固有のただのC文字列や一時的な作業用メモリ領域については、ZMMの管理下で `efree()` を用いて確実に回収する。

—

4. テックリードからの最終提言:メモリ安全性の担保

PHPの裏側でうごめくZend Engineは、私たちが想像する以上に洗練された、かつデリケートなメモリ管理機構を持っている。Webアプリケーションのリクエストがミリ秒単位で処理される現代において、メモリリークは単なる「リソースの無駄遣い」ではなく、OOM(Out of Memory) Killerによるプロセス突然死、ひいてはAPIの可用性低下を招く致命傷になり得る。

  • 「変数を片付けるときは `zval_ptr_dtor()`」
  • 「自分で確保したただのバイト列を片付けるときは `efree()`」

この2つの鉄則を脳裏に刻み込み、コードレビューの際には「このポインタが指すメモリの所有権(Ownership)は誰にあるのか?」を常に問い続けてほしい。その厳格さの積み重ねこそが、最高峰のWebシステムを支えるエンジニアの矜持である。

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