2022-04-05

[php-src読書録]その6: array

php-srcのコードリーディングした内容をコツコツ残すテスト。その6

今回はarray

定数配列

<?php

$i = [1, 2];

以下がopcodeで、arrayがそのままアサインされることになる

$_main:
; (lines=2, args=0, vars=1, tmps=1)
; (before optimizer)
; /path/to/6.php:1-4
; return  [] RANGE[0..0]
0000 ASSIGN CV0($i) array(...)
0001 RETURN int(1)

arrayは zend_compile_array() でコンパイルされる。 定数配列の場合、 zend_compile_array() 内の zend_try_ct_eval_array() でarrayのzvalをコンパイルしている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L9334

以下で全要素が定数であるかの確認
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8454-L8483

array_init_size() でarrayを初期化
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8494:L8494

マクロなどを全部展開するとこんな感じなコードになり、ハッシュテーブルをinitしてzvalにセットしている。

HashTable *ht = emalloc(sizeof(HashTable));
_zend_hash_init_int(ht, list->children, ZVAL_PTR_DTOR, 0);

zend_array *__arr = ht;
zval *__z = result;
__z.value.arr = __arr;
__z.u1.type_info = IS_ARRAY_EX;

各要素に対して zend_ast_get_zval() でzvalを取得する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8500

ここでresultにzvalをインサート
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L8559

ht->arData + h に配列としてデータを格納している
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_hash.c#L1077-L1084

変数入りの配列

<?php

$i = 1;
$j = [$i, 2];

opcodes

$_main:
     ; (lines=5, args=0, vars=2, tmps=3)
     ; (before optimizer)
     ; /path/to/6_2.php:1-5
     ; return  [] RANGE[0..0]
0000 ASSIGN CV0($i) int(1)
0001 T3 = INIT_ARRAY 2 (packed) CV0($i) NEXT
0002 T3 = ADD_ARRAY_ELEMENT int(2) NEXT
0003 ASSIGN CV1($j) T3
0004 RETURN int(1)
LIVE RANGES:
     3: 0002 - 0003 (tmp/var)

INIT_ARRAYで配列を初期化し、2番目以降の要素をADD_ARRAY_ELEMENTで設定している

コンパイルの該当箇所はこちらで、一番目の要素で ZEND_INIT_ARRAY して、2番目以降で ZEND_ADD_ARRAY_ELEMENT のopcodeを作っている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L9381-L9389

INIT_ARRAYは ZEND_INIT_ARRAY_SPEC_CV_UNUSED_HANDLER でハンドリングする。 処理内容はこんな感じ。でarray_initして一番目の要素をinsertしている

size = opline->extended_value >> ZEND_ARRAY_SIZE_SHIFT;
ZVAL_ARR(array, zend_new_array(size));

// ...
zend_hash_next_index_insert(Z_ARRVAL_P(EX_VAR(opline->result.var)), expr_ptr)

ADD_ARRAY_ELEMENTは ZEND_ADD_ARRAY_ELEMENT_SPEC_CONST_UNUSED_HANDLER でハンドリングされる。 こちらもINIT_ARRAYと同様に zend_hash_next_index_insert() でinsertされている(詳細は割愛)

要素取得と要素設定

<?php

$i = [1, 2];
$i[1];
$i[0] = 3;

opcodes

$_main:
     ; (lines=6, args=0, vars=1, tmps=3)
     ; (before optimizer)
     ; /path/to/6_3.php:1-6
     ; return  [] RANGE[0..0]
0000 ASSIGN CV0($i) array(...)
0001 T2 = FETCH_DIM_R CV0($i) int(1)
0002 FREE T2
0003 ASSIGN_DIM CV0($i) int(0)
0004 OP_DATA int(3)
0005 RETURN int(1)

ざっくり、FETCH_DIM_Rで配列要素の取得、ASSIGN_DIMで配列要素の設定をしている。

配列要素の取得のコンパイルでは zend_compile_dim() が呼ばれる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10227

このへんでFETCH_DIM_Rにコンパイルされている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L2851-L2857

ZEND_AST_DIMはこのへんでコンパイルされている
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3229-L3239

expr_nodeはOP_DATAのところでemitされていることに注意。ASSIGN_DIMはASSIGN_DIMとOP_DATAの2つのopcodeを利用するため(後述

FETCH_DIM_Rは ZEND_FETCH_DIM_R_SPEC_CV_CONST_HANDLER でハンドリングされる

value = zend_fetch_dimension_address_inner(Z_ARRVAL_P(container), dim, IS_CONST, BP_VAR_R EXECUTE_DATA_CC);
ZVAL_COPY_DEREF(EX_VAR(opline->result.var), value);

// ...
ZEND_HASH_INDEX_FIND(ht, hval, retval, num_undef);

ZEND_HASH_INDEX_FINDでハッシュテーブル(ht)からインデックス(hval)を指定して、retvalに値をセットしている。

ASSIGN_DIMは ZEND_ASSIGN_DIM_SPEC_CV_CONST_OP_DATA_CONST_HANDLER でハンドリングされる

variable_ptr = zend_fetch_dimension_address_inner_W_CONST(Z_ARRVAL_P(object_ptr), dim EXECUTE_DATA_CC);

value = RT_CONSTANT((opline+1), (opline+1)->op1);

/* assign_dim has two opcodes! */
ZEND_VM_NEXT_OPCODE_EX(1, 2);

zend_fetch_dimension_address_inner_W_CONST() では最終的に ZEND_HASH_INDEX_LOOKUP() が呼ばれて配列要素のポインタを取得する。

opline+1 というのは OP_DATA のことを指していて、expr、つまり値のデータを取得している。 本来 opline+1 して次のopcodeに処理を移すのだが、OP_DATA分をスキップするので ZEND_VM_NEXT_OPCODE_EX では opline+2 をしている。

このエントリーをはてなブックマークに追加