2022-04-04

[php-src読書録]その5: function

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

今回はfunction

<?php

function hoge($i) {}

hoge(123);

opcode

$_main:
     ; (lines=4, args=0, vars=0, tmps=1)
     ; (before optimizer)
     ; /path/to/5.php:1-6
     ; return  [] RANGE[0..0]
0000 INIT_FCALL 1 96 string("hoge")
0001 SEND_VAL int(123) 1
0002 DO_UCALL
0003 RETURN int(1)

hoge:
     ; (lines=2, args=1, vars=1, tmps=0)
     ; (before optimizer)
     ; /path/to/5.php:3-3
     ; return  [] RANGE[0..0]
0000 CV0($i) = RECV 1
0001 RETURN null

functionのコンパイル

zend_compile_top_stmt() から zend_compile_func_decl() が呼ばれる https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L9936

zend_compile_func_decl() では関数用のop_arrayを作成する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7172-L7292

zend_compile_func_decl()zend_begin_func_decl() を呼び出し
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7214:L7214

top_levelの場合は zend_hash_add_ptr() を呼び出し、 CG(function_table) のHashの中にop_arrayをセットしている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7150:L7150

さらに zend_compile_params() を呼び出し関数のパラメータのコンパイルを行います
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7256

パラメータはその関数のCVとしてコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6618-L6619

opcodeは ZEND_RECV
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6673-L6675

https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L6720:L6720

zend_compile_stmt() で関数内のステートメントをコンパイル
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L7268

呼び出し部分のコンパイル

ノードのtypeとしては ZEND_AST_CALL なので↓の部分を通る
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10095

最終的に zend_compile_call() が呼ばれてここでコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L10234

zend_compile_call() では zend_hash_find_ptr() を使って CG(function_table) から zend_function を取得する
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4458:L4458

ZEND_INIT_FCALL のopcodeを生成
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L4490-L4491

op1.numzend_vm_calc_used_stack() の値をセットする
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3710:L3710

最後に ZEND_DO_UCALL のopcodeがコンパイルされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_compile.c#L3667:L3667

OPCODE

INIT_FCALLでは ZEND_INIT_FCALL_SPEC_CONST_HANDLER を呼び出します。 EX(run_time_cache) にキャッシュされていたらそれを使い、キャッシュされていなければ EG(function_table) からzend_functionを取得します。

fbc = CACHED_PTR(opline->result.num);
if (UNEXPECTED(fbc == NULL)) {
	fname = (zval*)RT_CONSTANT(opline, opline->op2);
	func = zend_hash_find_known_hash(EG(function_table), Z_STR_P(fname));
	if (UNEXPECTED(func == NULL)) {
		ZEND_VM_TAIL_CALL(zend_undefined_function_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
	}
	fbc = Z_FUNC_P(func);
	if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!RUN_TIME_CACHE(&fbc->op_array))) {
		init_func_run_time_cache(&fbc->op_array);
	}
	CACHE_PTR(opline->result.num, fbc);
}

その後、呼び出す関数のCall Frameをプッシュし、呼び出し元のcallをprev_execute_dataに格納します。

call = _zend_vm_stack_push_call_frame_ex(
	opline->op1.num, ZEND_CALL_NESTED_FUNCTION,
	fbc, opline->extended_value, NULL);
call->prev_execute_data = EX(call);
EX(call) = call;
フィールド 意味
op1.num 確保するスタックのサイズ
op2->constant 関数名のliteral文字列
result.num cache_slot(多分キャッシュ用の何か)

SEND_VAL のopcodeでは ZEND_SEND_VAL_SPEC_CONST_UNUSED_HANDLER が呼ばれる。

arg = ZEND_CALL_VAR(EX(call), opline->result.var);
value = RT_CONSTANT(opline, opline->op1);
ZVAL_COPY_VALUE(arg, value);

op1の変数の値をresult.varに入れる。ZEND_CALL_VAR の引数が EX(call) になっていることで、 呼び出し先の関数のコールスタックのところに変数を代入する処理になっている。

DO_UCALL のopcodeでは ZEND_DO_UCALL_SPEC_RETVAL_UNUSED_HANDLER が呼ばれる。

call->prev_execute_data = execute_data;
execute_data = call;
i_init_func_execute_data(&fbc->op_array, ret, 0 EXECUTE_DATA_CC);

呼び出し先のコールスタックの prev_execute_data に呼び出し元のコールスタックを紐付け、 execute_dataを呼び出し先のコールスタックに変更している。

型情報がない場合や引数が不足していない場合、RECVはスキップされる
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.c#L3915-L3926

ZEND_RECV_SPEC_UNUSED_HANDLER では呼び出し引数が少なくないかどうかや、型チェックをしている。

static ZEND_VM_HOT ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_RECV_SPEC_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
	USE_OPLINE
	uint32_t arg_num = opline->op1.num;
	zval *param;

	if (UNEXPECTED(arg_num > EX_NUM_ARGS())) {
		ZEND_VM_TAIL_CALL(zend_missing_arg_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU));
	}

	param = EX_VAR(opline->result.var);

	if (UNEXPECTED(!(opline->op2.num & (1u << Z_TYPE_P(param))))) {
		ZEND_VM_TAIL_CALL(zend_verify_recv_arg_type_helper_SPEC(param ZEND_OPCODE_HANDLER_ARGS_PASSTHRU_CC));
	}

	ZEND_VM_NEXT_OPCODE();
}

RETURNでは zend_leave_helper_SPEC が呼ばれ、以下の処理が実行される。

EG(current_execute_data) = EX(prev_execute_data);
EG(vm_stack_top) = (zval*)execute_data;

current_execute_dataを呼び出し元のCall Frameに戻し、vm_stack_topをexecute_dataに戻す (設定前は execute_data + 関数のCall Frameのサイズ のアドレスを指している)

ちなみに return 文が入る場合は、i_init_func_execute_data()EX(return_value) にreturn用の変数アドレスをセットし、 ZVAL_COPY_VALUE(return_value, retval_ptr); で呼び出し先からreturn用変数にセットしている。
https://github.com/php/php-src/blob/PHP-8.1.4/Zend/zend_execute.c#L3910:L3910

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