PHP5とPHP7の違い（extension編）

PHP5用のextensionは変更なしではPHP7上で動きません。`#if`で分岐して両対応にするのも厳しい印象で、多くのextensionは別ブランチで開発しているようです（調べた範囲ではAPCu、msgpack、memcachedなどが別ブランチで開発しているようです）。

今回PHP7用の実験的なextension「[php7_explorer](https://github.com/hnw/php7_explorer)」を書いてみました。書いてみてPHP5用のextensionとの違いを改めて実感したので、ざっくりの違いをまとめてみます。

# 「おまじない」を書かなくてよくなった

PHP5までは、Zend APIのプロトタイプ宣言最後に`TSRMLS_DC`を、呼び出し時の引数リストの最後に`TSRMLS_CC`をつけるという「おまじない」が必要でした。これはZTS (Zend Thread Safety)サポートのため、必要なときだけ余計な引数を引き回すようなマクロ定義です。

これがPHP7の改修で不要になりました。ですからPHP7のextensionはスッキリ書けます。もちろん、PHP5/7両対応のextensionであれば今まで通りに書いても正しく動きます。

無くなったのは関数の引数リストの「おまじない」だけで、ZTSのサポート自体は残っています。また、引数リスト以外の場所の「おまじない」が若干変わっていたりしているので注意が必要です。

# いままで普通に使っていた型が無くなった

- `zend_uint` 廃止 → `uint32_t` を使うように

そのくせ`zend_ulong`とかは元気に残っていて、納得いかない気持ちになります。

# `zend_parse_parameters()`で文字列引数をparseしたときの型が変わった

`zend_parse_parameters()`の第2引数で`"s"`を指定すると、PHP関数の文字列型の引数を解釈し、Cの世界の変数値に変換することができます。

しかし、文字列の値を取るのにPHP5では`char **`と`int *`を渡して値を詰めてもらっていたのが、PHP7からは`char **`と`size_t *`に変わりました。`int`と`size_t`とは64bit環境だとサイズが異なるので、気付かずに使うと思わぬバグの原因になりかねません。注意しましょう。

# 文字列が`zend_string *`になったりならなかったり

一部のAPIで、 `const char *str, size_t str_len` の2引数が `zend_string *str` の1引数に変わっていたりします。別のAPIでは元通り2引数のままだったりして、どういう基準でzend_stringに変わっているのか僕にはわかりませんでした。

# 文字列の長さについて、最後のヌル文字を含めないように統一された

一部APIについて、`char *`文字列の長さの数え方が変わっています。たとえば、PHP5時代は

```c
add_assoc_bool_ex(&zv, "foo", sizeof("foo"), 0);
```

と書いていたのが、PHP7からは

```c
add_assoc_bool_ex(&zv, "foo", sizeof("foo") - 1, 0);
```

になります。上のようにPHP5とPHP7とでプロトタイプ宣言が同じなのに引数の意味が変わっている奴は凶悪ですよね。ついカッとなって`zend_string`を使うように盛大に書き換えてしまいそうです。

# ポインタが2段減ったり1段減ったり減らなかったり

PHP7では今までzvalを原則ポインタ渡ししていたのを値渡しするようになり、各種Zend APIの引数が盛大に変更されました。そのため、API呼び出し部分の大半を書き直す必要があります。

例として、ユーザー関数を呼び出すAPI `call_user_function_ex()` を見てみましょう。

```c
ZEND_API int call_user_function_ex(HashTable *function_table,
								   zval **object_pp,
								   zval *function_name,
								   zval **retval_ptr_ptr,
								   zend_uint param_count,
								   zval **params[],
								   int no_separation,
								   HashTable *symbol_table TSRMLS_DC);
```

これがPHP7では次のように変わっています。

```c
ZEND_API int call_user_function_ex(HashTable *function_table,
								   zval *object,
								   zval *function_name,
								   zval *retval_ptr,
								   uint32_t param_count,
								   zval params[],
								   int no_separation,
								   zend_array *symbol_table);
```

これは中々恐ろしいサンプルになっています。第2・第4引数は `zval **` から `zval *` とポインタが1段減っていますが、第3引数は`zval *`のまま変わっていません。一方で第6引数は `zval ***` が `zval *` になっていたりします。実は私も自信がないのですが、多分これで正しいんだと思います。

# `zend_hash_*()` 系APIが結構変わった

すでに紹介した内容ともかぶるんですが、ハッシュ操作系のAPIの大半で引数の個数や型が変わっており、PHP5用のextensionをPHP7対応させようと思うと結構手間がかかる印象です。たとえば `zend_hash_find()` などは引数で `zval ***` を渡して値を受け取っていたのが関数の返り値で `zval *` を返すようになっていたりして、 `#if` で切り替えられる限度を超えている印象です。

また、PHP5までは内部的なハッシュはポインタを管理しており、`zval *` やその他のポインタとして解釈していましたが、PHP7では `zval` を管理するようになりました。内部的な関数テーブルなど `zval` 以外のポインタ値を管理するときのために `zend_hash_find_ptr()` のような `_ptr` サフィックスがついたAPIが新設されています。

さらに、hash系のマクロ関数として`ZEND_HASH_FOREACH_VAL(ht, val)` のようなものが新規導入されています。名前の通りハッシュの全要素に対して処理を行うためのマクロなんですが、使ってみると非常に便利で、PHP5系にもバックポートして欲しいくらいですね。

# 自作のクラスに対応する構造体の作り方が変わった

extensionでクラスを作る場合、PHP5では下記のようにzend_objectを先頭に配置した構造体を作るのが一般的でした。

```
struct custom_object {
   zend_object std;
   void  *custom_data;
}
```

これに対し、PHP7では下記のようにzend_objectを末尾に配置する必要があります。

```
struct custom_object {
   void  *custom_data;
   zend_object std;
}
```

この構造体に対応するメモリ確保の方法も変わっており、下記のようなコードになっています。

```
     struct custom_object *intern = ecalloc(1, 
         sizeof(struct custom_object) + 
         zend_object_properties_size(ce));
```

これはzend_objectのproperties_tableを可変個にするためのトリックです。

他にもオブジェクト生成関数の返り値の型が変わっていたりして、若干ハマりどころになりそうです。

# 参照

- [Upgrading PHP extensions from PHP5 to NG](https://wiki.php.net/phpng-upgrading)
- [PHP RFC: Native TLS](https://wiki.php.net/rfc/native-tls)