php 5.3 の Cannot use string offset as an array の詳細

Qiita に投稿した内容ですが、次のコードは php 5.3 と 5.4 で結果が異なります。

$str = "hoge";
var_dump($str[0][0]);

5.4 の場合

string(1) "h"

5.3 の場合

Fatal error: Cannot use string offset as an array

もっと調べた

php 5.3 では $str[0] のような表現は内部的に一時的に 文字列オフセット? というものになるようです。

zend_execute.c:1086

result->str_offset.str = container; // container => $str
PZVAL_LOCK(container);
result->str_offset.offset = Z_LVAL_P(dim); // Z_LVAL_P(dim) => 0
result->var.ptr_ptr = NULL;
result->var.ptr = NULL;
  • container は元となる変数($str とか)、dim がオフセットの値です([0] とか)

そして 文字列オフセット に対して更に [0] のようなオフセット指定でアクセスすることは出来ません。

zend_vm_def.h:1078

container = _get_zval_ptr_ptr_var(&opline->op1, EX(Ts), &free_op1 TSRMLS_CC);
if (OP1_TYPE == IS_VAR && !container) {
	zend_error_noreturn(E_ERROR, "Cannot use string offset as an array");
}

zend_execute.c:287

static zend_always_inline zval **_get_zval_ptr_ptr_var(const znode *node, const temp_variable *Ts, zend_free_op *should_free TSRMLS_DC)
{
	zval** ptr_ptr = T(node->u.var).var.ptr_ptr;

	if (EXPECTED(ptr_ptr != NULL)) {
		PZVAL_UNLOCK(*ptr_ptr, should_free);
	} else {
		/* string offset */
		PZVAL_UNLOCK(T(node->u.var).str_offset.str, should_free);
	}
	return ptr_ptr;
}
  • 文字列オフセットに更にオフセット指定でアクセスすると _get_zval_ptr_ptr_var が NULL を返します

ただし、一旦 php の変数に入れてやれば 文字列オフセット が文字列に変換されるので、エラーにはなりません。

zend_execute.c:214

static zend_always_inline zval *_get_zval_ptr_var(const znode *node, const temp_variable *Ts, zend_free_op *should_free TSRMLS_DC)
{
	zval *ptr = T(node->u.var).var.ptr;
	if (EXPECTED(ptr != NULL)) {
		PZVAL_UNLOCK(ptr, should_free);
		return ptr;
	} else {
		return _get_zval_ptr_var_string_offset(node, Ts, should_free TSRMLS_CC);
	}
}
  • _get_zval_ptr_var_string_offset が zval* を返します
$v = "hoge";
$c = $v[0];
var_dump($c[0]);

php 5.4 では

php 5.4 では文字列にオフセット指定でアクセスした時点で 長さ 1 の文字列 になっているので、このエラーは発生しません。

zend_execute.c:1301

ALLOC_ZVAL(ptr);
INIT_PZVAL(ptr);
Z_TYPE_P(ptr) = IS_STRING;

if (Z_LVAL_P(dim) < 0 || Z_STRLEN_P(container) <= Z_LVAL_P(dim)) {

	: 
	
	Z_STRVAL_P(ptr) = (char*)emalloc(2);
	Z_STRVAL_P(ptr)[0] = Z_STRVAL_P(container)[Z_LVAL_P(dim)]; // $buf[0] = $str[0];
	Z_STRVAL_P(ptr)[1] = 0; // $buf[0] = '\0';
	Z_STRLEN_P(ptr) = 1; // $buf.length = 1;
}

いくらでも [0] を繋げられます。

<?php
$str = "hoge";
var_dump($str[0][0][0][0][0][0][0][0][0][0][0]); // string(1) "h"

ただし、読み込みのときだけです。書き込もうとすると同じエラーになります。

<?php
$str = "hoge";
$str[0][0] = 'a'; // Fatal error: Cannot use string offset as an array

最後に

エラーメッセージでググった感じ、元は PHP 4 から 5 のときに Fatal error になるようになったようです。

恐らく、php 5.4 で配列のデフィファレンスの辺りの変更の副作用で Fatal error にならなくなったのだと思います。