ジョブカン事業部のアドベントカレンダー20日目の記事です。
19日目は@marmot_0820さんによる「Cursorで爆速テスト設計!Git差分からテスト観点を抽出する方法」という記事でした。
生成AIという新しい技術に対してアンテナを張って、日々の業務に活かせる方法を模索している姿勢が素晴らしいなと思いました。ご興味ある方は是非ご一読ください。
はじめに
私が携わっているプロダクトでは一部システムでPHP言語を採用しています。
普段の開発で言語の内部実装を意識することはあまりありません。しかし、パフォーマンス改善やトラブルシューティングといった場面では、しばしば言語内部の仕様や実装についての理解が求められます。
そこで、今回はアドベントカレンダーという機会を活用して、PHPがプログラムを実行するまでの流れについて調べてみました。
PHPは歴史ある言語であり、その内部実装や構成を網羅的に解説するのは容易でありません。そこで本記事では、以下の2点に焦点を絞って解説したいと思います。
- PHPプログラムから生成されるオペコードはどういったものか
- オペコードが生成され、実行されるまでの流れはどうなっているか
OPcacheやJITコンパイルについてはそれ単体で記事を書けるくらいのボリューム感ある内容だと思うので、本記事のスコープ外とします。
前提
対象読者
- PHP言語内部の実装や仕組みを理解した上で、PHP言語を扱えるようになりたい方
- 言語処理系に興味があり、何かしらのプログラミング言語の内部実装について知りたい方
利用する環境
PHPバージョン
PHP 8.5.0 (cli) (built: Nov 28 2025 12:24:17) (NTS DEBUG)
Copyright (c) The PHP Group
Zend Engine v4.5.0, Copyright (c) Zend Technologies
with Zend OPcache v8.5.0, Copyright (c), by Zend Technologies
つい先日PHP8.5がリリースされました ![]()
パイプ演算子など魅力的な言語機能が新しく追加されています。
with Zend OPcache v8.5.0, Copyright (c), by Zend Technologies
PHP8.5からOPcacheが必須拡張コンポーネント(PHPをインストールしたら同梱される拡張コンポーネント)になりました。ただし、opcache.enable=0といった設定次第で実際にOPcacheを利用するか否かは設定できます。
SAPI
PHPを外部から利用するためのインターフェースをServer API(SAPI)と呼びます。本記事では挙動を追いやすいという理由からcli SAPIを利用したいと思います。
SAPIには他にも
- Apacheから利用する際のapache2handler
- PHP-FPMから利用する際のfpm
などが存在します。詳しくは
をご覧下さい。
PHPプログラムが実行されるまでの流れ
プログラミング言語をコンパイル言語 / インタプリタ言語の二つに分けた時、PHPはインタプリタ言語に分類されます。即ち、プログラムを事前に機械語へコンパイルするのではなく、実行時に解釈しながら処理する形態です。
インタプリタ言語にもグラデーションがあり、ソースコードを直接解釈実行するものもあれば、何らかの中間表現(バイトコードなど)にコンパイルし、それを解釈実行するものもあります。
PHPは後者でオペコードと呼ばれるバイトコードを生成し、それをZend Engine(正確にはZend Virtual Machine / Zend VM)上で解釈実行するようなアプローチを採用しています。
また、プログラム高速化の仕組みとしてオペコードをキャッシュするOPcacheやプログラム実行中に機械語へコンパイルするJust-In-Time(JIT)コンパイルなどの機能が実装されています。
オペコードという言葉は一般的に、操作に関する命令コードを指し、操作対象を指すオペランドと対となる概念として用いられることが多いかと思います。一方でPHPの内部実装ではオペコードとオペランドをまとめた命令単位(zend_op構造体)を指してオペコードと呼ばれることが多いです。
本記事で使われているオペコードは、その広い意味でのオペコードだと思って読んで頂けると幸いです。
PHPでプログラムを実行する流れを図示すると以下のようになります(冒頭でお伝えした通り、OPcacheやJIT、その他最適化処理については含めていません)。
字句解析
字句解析ではプログラムを言語仕様に沿ったトークンへ分割します。
構文解析
構文解析では字句解析より渡されたトークン列が言語の文法に沿っているかを検査すると共に、プログラムを木構造で表現した抽象構文木を構築します。
コンパイル / 仮想マシン
コンパイラでは抽象構文木をオペコードと呼ばれるバイトコードに変換します。オペコードはZend VM上で解釈され、各オペコードに対応するハンドラで処理します。
本記事では、
- オペコードが実際どのようなものなのか
- どのような流れでオペコードが生成、実行されるのか
といった部分を後続の章で解説していきます。
実際にオペコードを眺めてみる
ここでは実際に以下のPHPスクリプトをオペコードにコンパイルし、その結果を眺めていきたいと思います。
<?php
function greeting($name) {
return "Hello, " . $name . "!";
}
echo greeting("World");
オペコードのみを確認しても良いのですが、先ほどPHPプログラムが実行される流れを紹介したので、今回はその流れに沿って
- 字句解析結果であるトークン列
- 構文解析結果である抽象構文木
- コンパイル結果であるオペコード
の3段階に分けて確認していきます。
トークン列
トークンを確認する方法として、今回はPHPが提供しているtoken_get_all関数を利用したいと思います。
token_get_all関数に引数として上記プログラム(を文字列化したもの)を渡して実行した結果が以下の通りです。
$ ./sapi/cli/php ./test_tokens.php
Line 1: T_OPEN_TAG ('<?php
')
Line 2: T_WHITESPACE ('
')
Line 3: T_FUNCTION ('function')
Line 3: T_WHITESPACE (' ')
Line 3: T_STRING ('greeting')
Char: '('
Line 3: T_VARIABLE ('$name')
Char: ')'
Line 3: T_WHITESPACE (' ')
Char: '{'
Line 3: T_WHITESPACE ('
')
Line 4: T_RETURN ('return')
Line 4: T_WHITESPACE (' ')
Line 4: T_CONSTANT_ENCAPSED_STRING ('"Hello, "')
Line 4: T_WHITESPACE (' ')
Char: '.'
Line 4: T_WHITESPACE (' ')
Line 4: T_VARIABLE ('$name')
Line 4: T_WHITESPACE (' ')
Char: '.'
Line 4: T_WHITESPACE (' ')
Line 4: T_CONSTANT_ENCAPSED_STRING ('"!"')
Char: ';'
Line 4: T_WHITESPACE ('
')
Char: '}'
Line 5: T_WHITESPACE ('
')
Line 7: T_ECHO ('echo')
Line 7: T_WHITESPACE (' ')
Line 7: T_STRING ('greeting')
Char: '('
Line 7: T_CONSTANT_ENCAPSED_STRING ('"World"')
Char: ')'
Char: ';'
Line 7: T_WHITESPACE ('
')
トークン出力スクリプト
<?php
# ./test.phpを読み込む
$program = file_get_contents('./test.php');
# token_get_allを使ってトークンを取得
$tokens = token_get_all($program);
# トークンを出力
foreach ($tokens as $token) {
if (is_array($token)) {
echo "Line {$token[2]}: ", token_name($token[0]), " ('{$token[1]}')", PHP_EOL;
} else {
echo "Char: '{$token}'", PHP_EOL; // 単一文字トークン
}
}
T_FUNCTION, T_WHITESPACE, T_RETURNといったトークンに分割されていることを確認できます。プログラム上同じように見える文字の列も、それぞれ異なるトークンとして識別されています。
抽象構文木
抽象構文木を確認する方法としては、PHP-Parserというライブラリを利用します。
PHP言語を利用して抽象構文木を構築するライブラリであるため、出力されるものにはPHP言語内部で構築されるそれとは少し異なる部分がある点にご留意下さい。
上記ライブラリを利用すると以下のような出力が得られます。
array(
0: Stmt_Function(
attrGroups: array(
)
byRef: false
name: Identifier(
name: greeting
)
params: array(
0: Param(
attrGroups: array(
)
flags: 0
type: null
byRef: false
variadic: false
var: Expr_Variable(
name: name
)
default: null
hooks: array(
)
)
)
returnType: null
stmts: array(
0: Stmt_Return(
expr: Expr_BinaryOp_Concat(
left: Expr_BinaryOp_Concat(
left: Scalar_String(
value: Hello,
)
right: Expr_Variable(
name: name
)
)
right: Scalar_String(
value: !
)
)
)
)
)
1: Stmt_Echo(
exprs: array(
0: Expr_FuncCall(
name: Name(
name: greeting
)
args: array(
0: Arg(
name: null
value: Scalar_String(
value: World
)
byRef: false
unpack: false
)
)
)
)
)
)
抽象構文木出力スクリプト
<?php
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;
require __DIR__ . '/vendor/autoload.php';
$code = file_get_contents(__DIR__ . '/test.php');
$parser = (new ParserFactory())->createForNewestSupportedVersion();
$ast = $parser->parse($code);
$dumper = new NodeDumper();
echo $dumper->dump($ast) . PHP_EOL;
テキストベースだと理解しづらいかと思うので、Graphvizというツールを利用して図示してみました。
DOT言語による可視化スクリプト
digraph PHP_AST {
graph [rankdir=TB, nodesep=0.25, ranksep=0.3];
node [shape=box, fontsize=11, fontname="Helvetica"];
edge [fontsize=10, fontname="Helvetica"];
root [label="stmts"];
# 0: Stmt_Function
fn [label="Stmt_Function"];
root -> fn;
fn_name [label="Identifier\nname: greeting"];
fn -> fn_name;
fn_params [label="params"];
fn -> fn_params;
param0 [label="Param"];
fn_params -> param0;
param0_var [label="Expr_Variable\nname: name"];
param0 -> param0_var;
fn_stmts [label="stmts"];
fn -> fn_stmts;
ret [label="Stmt_Return"];
fn_stmts -> ret;
# Return expr: Concat( Concat(\"Hello, \", $name), \"!\")
ret_expr [label="Expr_BinaryOp_Concat"];
ret -> ret_expr;
concat_left_outer [label="left"];
ret_expr -> concat_left_outer;
concat_left [label="Expr_BinaryOp_Concat"];
concat_left_outer -> concat_left;
left_left_outer [label="left"];
concat_left -> left_left_outer;
hello [label="Scalar_String\nvalue: \"Hello, \""];
left_left_outer -> hello;
left_right_outer [label="right"];
concat_left -> left_right_outer;
var_in_concat [label="Expr_Variable\nname: name"];
left_right_outer -> var_in_concat;
concat_right_outer [label="right"];
ret_expr -> concat_right_outer;
excl [label="Scalar_String\nvalue: \"!\""];
concat_right_outer -> excl;
# 1: Stmt_Echo
echo [label="Stmt_Echo"];
root -> echo;
echo_exprs [label="exprs"];
echo -> echo_exprs;
echo_expr0 [label="Expr_FuncCall"];
echo_exprs -> echo_expr0;
call_name [label="Name\nname: greeting"];
echo_expr0 -> call_name;
call_args [label="args"];
echo_expr0 -> call_args;
arg0 [label="Arg"];
call_args -> arg0;
arg0_value [label="Scalar_String\nvalue: \"World\""];
arg0 -> arg0_value;
}
オペコード
オペコードをダンプする方法は色々ありますが、今回はOPcacheの機能を活用したいと思います。
$ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x10000 test.php
debug_levelとして指定している0x10000は最適化前のオペコードを出力するための設定です。
実行した結果は以下になります。
$_main:
; (lines=5, args=0, vars=0, tmps=1)
; (before optimizer)
; /app/main.php:1-7
; return [] RANGE[0..0]
0000 INIT_FCALL 1 128 string("greeting")
0001 SEND_VAL string("World") 1
0002 V0 = DO_UCALL
0003 ECHO V0
0004 RETURN int(1)
greeting:
; (lines=5, args=1, vars=1, tmps=2)
; (before optimizer)
; /app/main.php:3-5
; return [] RANGE[0..0]
0000 CV0($name) = RECV 1
0001 T1 = CONCAT string("Hello, ") CV0($name)
0002 T2 = CONCAT T1 string("!")
0003 RETURN T2
0004 RETURN null
関数単位でオペコードがまとまって記載されており、今回の場合では$_mainとgreetingの二つのまとまりで出力されています。前者が実行プログラム直下の処理に関するオペコードで後者がgreeting関数に関するオペコードです。
各関数に対する出力はオペコードの他にメタ情報も含んでいます。まずは$_mainから見ていきましょう。
$_main:
; (lines=5, args=0, vars=0, tmps=1)
; (before optimizer)
; /app/main.php:1-7
; return [] RANGE[0..0]
0000 INIT_FCALL 1 128 string("greeting")
0001 SEND_VAL string("World") 1
0002 V0 = DO_UCALL
0003 ECHO V0
0004 RETURN int(1)
冒頭のメタ情報については、以下にコメントとして記載します。
# lines: PHPソースコードの行数ではなく、生成されたオペコードの数
# args: 引数の数
# vars: ローカル変数の数. コンパイル時にスロットに割り当てられるCompiled Variable(CV)の数
# tmps: 計算過程等で一時的に利用されるスロットの数(TMPVAR、VAR)
; (lines=5, args=0, vars=0, tmps=1)
; (before optimizer)
; /app/main.php:1-7
; return [] RANGE[0..0]
次に各オペコードについてもコメントとして説明を載せておきます。
# ファンクションコールの初期化
0000 INIT_FCALL 1 128 string("greeting")
# 文字列Worldを上記関数に送信. 1は第一引数であることを意味する
0001 SEND_VAL string("World") 1
# 関数実行した結果を一時変数に格納
0002 V0 = DO_UCALL
# 変数の値を出力
0003 ECHO V0
# メインスクリプトの戻り値として1を返す
0004 RETURN int(1)
次にgreetingのオペコードに関してもコメントとして説明を載せておきます(メタ情報については省略します)。
# 一つ目の引数の値を変数に格納
0000 CV0($name) = RECV 1
# 文字列"Hello, "と変数$nameの値を結合し、一時変数T1に格納
0001 T1 = CONCAT string("Hello, ") CV0($name)
# 変数T1と文字列"!"を結合し、一時変数T2に格納
0002 T2 = CONCAT T1 string("!")
# 変数T2の値を返却
0003 RETURN T2
# nullを返却
0004 RETURN null
因みに、debug_levelとして0x20000を指定することで、最適化後のオペコードを出力することができます。
$ php -dopcache.enable_cli=1 -dopcache.opt_debug_level=0x20000 test.php
実行した結果は以下のようになります。
$_main:
; (lines=5, args=0, vars=0, tmps=1)
; (after optimizer)
; /app/main.php:1-7
0000 INIT_FCALL 1 128 string("greeting")
0001 SEND_VAL string("World") 1
0002 V0 = DO_UCALL
0003 ECHO V0
0004 RETURN int(1)
greeting:
; (lines=4, args=1, vars=1, tmps=2)
; (after optimizer)
; /app/main.php:3-5
0000 CV0($name) = RECV 1
0001 T2 = CONCAT string("Hello, ") CV0($name)
0002 T1 = FAST_CONCAT T2 string("!")
0003 RETURN T1
今回はサンプルとして利用したスクリプトが簡素なものなので、最適化前後での差分をあまり確認できないですが、それでも以下のような差分を確認できるかと思います。
- 文字列結合のオペコードとしてFAST_CONCATが利用されている
- greeting末尾のRETURN nullが省略されている
オペコードが生成、実行される仕組み
$ ./sapi/cli/php test.php
のような形でPHPスクリプトをcli SAPIで実行した場合の流れを追っていきたいと思います。実行対象であるtest.phpは一つ前の章で利用したPHPプログラムを指しています。
cli SAPIを利用しているのでエントリポイントは以下のmain関数になります。
初期化
処理の最初の部分では
- 各種初期化
- コマンドラインオプションの解析
などが行われます。重要な処理としては下記php_module_startup関数の中で
- Garbage Collectorの初期化
- Zend Engineの起動
- Zend VMの初期化
- 設定ファイルphp.iniの読み込み
- 拡張モジュールの起動
などが実行されます。
この初期化処理はPHPライフサイクルにおけるModule Initialization(MINIT)に該当します。
cli SAPIだと単一リクエストでプロセスが終了するのですが、apache2handlerやfpm SAPIを利用したWeb環境だと一つのプロセスが複数リクエストを処理することになります。後者のような環境ではリクエスト毎にライフサイクルが存在するのですが、そのような複数のリクエストライフサイクルが共有して参照するオブジェクトや情報についてのロードやアロケートを行うのがMINITになります。
詳しくは下記資料をご参照ください。
https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html
スクリプト実行処理
初期化処理が終了すると、次はPHPスクリプトの実行処理に移ります。その処理の中でzend_execute_script関数について詳しくみていきます。
主な処理の流れは
- zend_compile_fileを実行してop_arrayを取得する
- 取得したop_arrayをzend_execute関数にわたす
となっています。このことからも分かる通りZend VMはZend CompilerとZend Executorという二つのコンポーネントで構成されています。
コンパイル処理(Zend Compiler)
字句解析、構文解析、AST構築
zend_compile_file関数自体は関数ポインタでその関数の実体は以下のファイルに定義されたcompile_file関数になります。zend_language_scanner.lファイルはre2cというLexer(字句解析器)ジェネレーターによりビルド時にC言語のソースが生成され、実際に使用されるのはそちらのファイルにて定義された関数になります。
OPcacheが有効な場合は、処理冒頭でこのzend_compile_fileの関数ポインタがOPcache側に差し替えられ、以降のコンパイルはOPcacheのフック経由で行われます。
/* Override compiler */
accelerator_orig_compile_file = zend_compile_file;
zend_compile_file = persistent_compile_file;
zend_compile_file関数内で呼び出す関数の中で重要なものとしては
- open_file_for_scanning
a. スキャナの初期化 - zend_compile
2. 解析処理
の2つが挙げられます。
2つ目のzend_compile関数では、zendparse関数内で抽象構文木(AST)を構築。構築に成功したら、後続の処理でASTをオペコードへと落とし込みます。オペコードへの落とし込みは具体的にはzend_op_array型のop_arrayという変数にデータを代入する処理が該当します。
zendparse関数の実体はzend_language_parser.yからBisonというParser(構文解析器)ジェネレーターにより生成されるzend_language_parser.cというファイル内で定義されているyyparseという関数になります。AST構築の流れはBisonやre2cについての理解が必要になってくるので、今回はスキップします。
オペコードへの落とし込み
オペコードへの落とし込みについて見ていきましょう。まず、zend_compile_file関数の戻り値であるzend_op_array構造体のデータ構造を確認します。
この構造体はコンパイル済みのオペコードを表現する構造体で、一つの実行可能なコード単位(トップレベルのコード、関数、メソッド)で表現します。
重要なメンバーをピックアップして説明しておきます。
struct _zend_op_array {
...
uint8_t type; /* 関数の種類 */
zend_string *function_name; /* 関数名 */
zend_class_entry *scope; /* 所属クラス(メソッドの場合)*/
zend_arg_info *arg_info; /* 引数の情報 */
zend_op *opcodes; /* 実際の命令列 */
zend_string **vars; /* コンパイル済み変数(CV)の情報 */
zval *literals; /* 定数データ */
...
};
オペコードへの落とし込む処理は、具体的には_zend_op_array構造体のopcodesメンバにオペコードを表現するzend_op型の配列を割り当てる処理が該当します。ですので、以下ではそのopcodesメンバにデータを挿入する過程を説明します。
zend_compile_top_stmt関数から落とし込み処理が始まります。
if (ast->kind == ZEND_AST_xxx) {
といった形で、ASTの各ノードの種別(kindメンバー)の値に応じて適当なオペコード生成関数を呼び出しています。
ASTのノード種別に対応したオペコード生成関数は多数あるので、オペコード生成に直接関連する関数を先に見ていこうと思います。重要な関数は以下の2つの関数です。
- get_next_op関数
- 1命令分のスロット確保
- opcodes配列の拡張
- 確保したスロットの初期化
- zend_emit_op関数
- 命令生成
このzend_emit_op関数を上述したASTノード種別に対応したオペコード生成関数(zend_compile_xxx関数)から呼び出して、適当なオペコードを生成するようになっています。
全てのオペコード生成関数を紹介することはできないですが、「実際にオペコードを眺めてみる」で扱ったテストスクリプトにも出現したecho関数についてを一例として紹介したいと思います。
echo関数に対応したオペコード生成関数はzend_compile_echo関数です。
zend_emit_op関数にZEND_ECHOというオペコード識別子を指定して、オペランドにはコンパイルした式を渡しています。
zend_op *opline;
zend_ast *expr_ast = ast->child[0];
でオペランド(ECHOオペコードの引数として渡すデータ)になるASTノードを取得。
znode expr_node;
zend_compile_expr(&expr_node, expr_ast);
でオペランドをコンパイルし、
opline = zend_emit_op(NULL, ZEND_ECHO, &expr_node, NULL);
opline->extended_value = 0;
でオペコードの生成(オペコード種別、オペランドの指定など)を行います。
実行処理(Zend Executor)について
zend_execute_script関数をおさらいしておくと、ここまで見てきたzend_compile_file関数を実行してzend_op_arrayを取得し、それをzend_execute関数に渡して実行していることが分かります。
以降では、このzend_execute関数の中身を探っていきます。
zend_execute関数はzend_vm_execute.hファイルにて定義されています。そのファイルはコード行数が13万行ほどあり、非常にサイズの大きなファイルになっています。
ファイルサイズの関係上、ファイル内の特定行へのリンクを取得できなかったので、以下にzend_execute関数の定義を転記しておきます。
ZEND_API void zend_execute(zend_op_array *op_array, zval *return_value)
{
zend_execute_data *execute_data;
void *object_or_called_scope;
uint32_t call_info;
if (EG(exception) != NULL) {
return;
}
object_or_called_scope = zend_get_this_object(EG(current_execute_data));
if (EXPECTED(!object_or_called_scope)) {
object_or_called_scope = zend_get_called_scope(EG(current_execute_data));
call_info = ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE;
} else {
call_info = ZEND_CALL_TOP_CODE | ZEND_CALL_HAS_SYMBOL_TABLE | ZEND_CALL_HAS_THIS;
}
execute_data = zend_vm_stack_push_call_frame(call_info,
(zend_function*)op_array, 0, object_or_called_scope);
if (EG(current_execute_data)) {
execute_data->symbol_table = zend_rebuild_symbol_table();
} else {
execute_data->symbol_table = &EG(symbol_table);
}
EX(prev_execute_data) = EG(current_execute_data);
i_init_code_execute_data(execute_data, op_array, return_value);
ZEND_OBSERVER_FCALL_BEGIN(execute_data);
zend_execute_ex(execute_data);
/* Observer end handlers are called from ZEND_RETURN */
zend_vm_stack_free_call_frame(execute_data);
}
因みに、zend_vm_execute.hファイルとオペコードを定義しているzend_vm_opcodes.h、zend_vm_opcodes.cの3つのファイルはzend_vm_gen.phpというPHPスクリプトを実行することにより自動生成されています。
$ ./sapi/cli/php ./Zend/zend_vm_gen.php
zend_vm_opcodes.h generated successfully.
zend_vm_opcodes.c generated successfully.
zend_vm_execute.h generated successfully.
スクリプトを実行することでファイルが自動生成されていることを確認できます。
zend_execute関数内の処理をざっくりと説明すると
- 初期化
- コールフレームの初期化
- 関数、メソッド呼び出しごとに積むフレーム
- zend_compile.hにある_zend_execute_dataに該当
- 実行中のオペコード命令や関数についてのメタ情報
- シンボルテーブルとの紐づけ
- シンボルテーブルは変数を格納するデータ構造
- コールフレームの初期化
- 実行ループ
- zend_execute_ex
- これはマクロで、その実体はexecute_ex関数
- execute_ex関数もzend_vm_execute.hファイルで定義されている
- 各オペコードに応じたハンドラを呼び出す
- zend_execute_ex
という感じになっています。
オペコードハンドラの実装はオペコードに応じて変わるので、その実装内容は多種多様です。すべてのハンドラ実装をここで紹介することはできないのですが、ここでは一例としてコンパイル処理でも取り上げたecho関数に関連したオペコードの実行処理の流れを追っていきたいと思います。
オペコードハンドラについても先ほどの自動生成されるzend_vm_execute.hファイルに定義されています。それではECHOオペコードについてのハンドラを見てみましょう。
static ZEND_OPCODE_HANDLER_RET ZEND_OPCODE_HANDLER_FUNC_CCONV ZEND_ECHO_SPEC_TMPVAR_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
USE_OPLINE
zval *z;
SAVE_OPLINE();
z = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC);
if (Z_TYPE_P(z) == IS_STRING) {
zend_string *str = Z_STR_P(z);
if (ZSTR_LEN(str) != 0) {
zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
}
} else {
zend_string *str = zval_get_string_func(z);
if (ZSTR_LEN(str) != 0) {
zend_write(ZSTR_VAL(str), ZSTR_LEN(str));
} else if ((IS_TMP_VAR|IS_VAR) == IS_CV && UNEXPECTED(Z_TYPE_P(z) == IS_UNDEF)) {
ZVAL_UNDEFINED_OP1();
}
zend_string_release_ex(str, 0);
}
zval_ptr_dtor_nogc(EX_VAR(opline->op1.var));
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}
処理の流れとしては、オペランドの実行結果を取得し、それをzend_stringという文字列型に変換し、zend_write関数を使って出力しています。
zend_write関数は関数ポインタのマクロで、関数ポインタに代入される箇所を探していくと
php_output_write関数にたどり着きます。この関数では更にphp_output_op関数を呼び出していて、その関数の中でも更にsapi_module構造体のub_writeメンバを実行していることがわかります。
_sapi_module_struct構造体のub_writeメンバも関数ポインタになっています。今回利用しているcli SAPIの場合だと、ub_writeメンバにsapi_cli_ub_write関数を代入していることが分かります。
sapi_cli_ub_write関数は内部でsapi_cli_single_write関数を呼び出しています。
そして、sapi_cli_single_write関数で最終的にC言語のライブラリで提供されているwrite(unistd.h)あるいはfwrite(stdio.h)関数を実行して、標準出力にオペランドの文字列を出力しています。
ここまでの流れを踏まえて、今回用意したサンプルスクリプトを再度実行してみましょう。
<?php
function greeting($name) {
return "Hello, " . $name . "!";
}
echo greeting("World");
$ ./sapi/cli/php test.php
Hello, World!
内部の実装を知っている状態と知らない状態とでは、ただの「Hello, World!」も見え方が変わってきますね...
また、PHPという高級言語のecho関数が呼び出されるまでの流れを辿っていくと、最終的にwrite関数というシステムコールにまでたどり着くのも感慨深いですね。
以上がPHPスクリプトをオペコードにコンパイルし仮想マシン上で実行する流れでした。
おわりに
PHP言語のソースコードリーディングは、その言語の歴史や思想を垣間見ることができ、非常に面白い体験でした。今回はOPcacheやJIT、最適化処理については省略したので、機会があればそれらの内部実装も調べてみようと思います。
最後になりますが、DONUTSでは新卒中途問わず積極的に採用活動を行っています。
詳細は下記リンク先よりご確認ください!
参考資料

