首页
/ 深入理解 PHP 解释器 php-src:从四阶段编译管线到核心数据结构与文档体系

深入理解 PHP 解释器 php-src:从四阶段编译管线到核心数据结构与文档体系

2026-09-05 12:59:31作者:宗隆裙

php-src 是 PHP 编程语言解释器的权威(canonical)实现,也是附带多个常用扩展的完整代码库。本文以官方内部文档入口 index.rst 为主体,系统梳理 PHP 解释器从源码到机器执行的完整处理管线(词法分析、语法分析、编译、解释执行)、核心数据结构(zval、引用计数、zend_string 等)、扩展与测试开发要点,并给出本地构建 Sphinx 文档的完整操作流程。读完本文,你将能够看懂 php-src 的整体架构地图、独立构建官方文档,并为进一步阅读 C 源码和贡献扩展打下基础。

一、文档定位与内容组织

官方文档首页(docs/source/index.rst)开宗明义地说明了三件事:

  1. php-src 是什么:PHP 解释器的权威实现,附带提供常用功能的各个扩展(extensions);
  2. 文档目标:帮助读者理解解释器如何工作(how the interpreter works)、如何构建和测试自己的修改(how to build and test changes)、以及如何自己动手编写扩展(how to create extensions yourself);
  3. 文档边界:本文档不追求面面俱到(not intended to be comprehensive),而是聚焦于"仅靠阅读代码难以掌握"的核心概念,描述最佳实践,并会刻意省略那些"不鼓励通用使用"的 API。

首页同时以警告框(.. warning::)提示:这套文档仍处于**进行中(work in progress)**状态;在更完整的替代方案出现之前,读者可以查阅 CONTRIBUTING.md 中列出的其他技术资源来获取更完整的 PHP 项目知识图景。

1.1 目录结构:三个 toctree 分区

首页通过三个 Sphinx toctree 指令组织了全部文档骨架,从源码结构看,这对应 docs/source/ 目录下的三个子目录:

分区(caption) 文档文件 覆盖主题
Introduction(入门) high-level-overview.rstides/index.rst 解释器高层架构概览、常见 IDE 使用指南
Core(核心) data-structures/index.rst zval、引用计数、zend_string、zend_constant 等核心数据结构
Miscellaneous(杂项) stubs.rstwriting-tests.rstrunning-tests.rst stub 机制、编写测试、运行测试

各分区内容要点如下:

  • 高层概览high-level-overview.rst):解释 PHP 作为解释型语言的定位,并以 tokenizer → parser → compiler → interpreter 的四阶段管线为主线展开;
  • IDE 指南ides/index.rst):介绍如何高效使用常见 IDE 进行 php-src 开发,目前收录了 Visual Studio Code 一节;
  • 数据结构data-structures/index.rst):以"概览 + 四篇子文档"的形式组织——zvalreference-countingzend_stringzend_constant
  • 测试体系writing-tests.rstrunning-tests.rst):覆盖 phpt 测试的命名规范、各 section 语法(TEST/FILE/EXPECT/SKIPIF/CLEAN 等)以及 make test / run-tests.php 的执行细节。

二、前置知识要求:先学 C,再懂 PHP 语义

首页的 "Prerequisites" 一节提出了两条明确的入门前提,这也是阅读 CONTRIBUTING.md 前最该建立的认知:

  1. C 语言基础是必需的。php-src 解释器本身用 C 编写,绑定的大多数扩展同样用 C 实现;目前唯一的例外是 ext-intl,它是当前唯一用 C++ 编写的捆绑扩展(对应 ext/intl/ 目录,其中含 69 个 .cpp 文件);
  2. 熟悉 PHP 语言语义本身。理解 PHP 的行为语义能帮助你区分"真正的 bug"与"预期行为",也为设计新的语言特性(model new language features)提供参照系。

这两点对应的实操含义是:修改 Zend/ 下的 C 代码前,你需要先建立对 PHP 运行时行为的直觉;而仓库本身也提供了 CODING_STANDARDS.md 来约束 C 代码风格。

三、解释器四阶段管线:token → AST → opcodes → 执行

这是整份文档的技术核心(high-level-overview.rst)。PHP 是解释型语言——源文件不会像编译型语言那样提前转成机器码,而是在运行时被读取、处理并解释。这虽然省去了漫长的编译阶段、便于快速原型开发,但也带来独特的性能挑战——这正是解释器复杂的根本原因之一。php-src 借鉴了编译器/解释器领域的许多通用概念。

文档用一个类 Haskell 风格的流水线图概括了整个流程:

source_code
  |> tokenizer   -- tokens
  |> parser      -- ast
  |> compiler    -- opcodes
  |> interpreter

3.1 词法分析(Tokenization / Lexing / Scanning)

词法分析将整份源文件切分为"单词与符号"的序列。每个 token 由一个类型(一个整型常量)和一个 lexeme(源码中的字面字符串)组成。文档给出的示例是把这段代码:

if ($cond) {
    echo "Cond is true\n";
}

切分为如下 token 序列:

T_IF                       "if"
T_WHITESPACE               " "
                           "("
T_VARIABLE                 "$cond"
                           ")"
T_WHITESPACE               " "
                           "{"
T_WHITESPACE               "\n    "
T_ECHO                     "echo"
T_WHITESPACE               " "
T_CONSTANT_ENCAPSED_STRING '"Cond is true\n"'
                           ";"
T_WHITESPACE               "\n"
                           "}"

实现层面:虽然手写 tokenizer 并不难,但 PHP 使用 re2c 工具自动生成高效 C 代码,从字符流中构造这些 token。PHP 的词法定义文件位于 Zend/zend_language_scanner.l(配套头文件为 Zend/zend_language_scanner.h),该文件确实存在于仓库中,.l 后缀即 re2c 输入约定。

3.2 语法分析(Parsing):从 token 到 AST

解析阶段读取 token 并构建树形结构。对人类来说,代码元素的分组靠空白和 (){} 等符号一眼可辨;但计算机无法"扫一眼"就确定这些边界,因此需要把 token 组织成更贴近人类视角的树——抽象语法树(AST)

上例 token 对应的简化 AST 如下:

ZEND_AST_IF {
    ZEND_AST_IF_ELEM {
        ZEND_AST_VAR {
            ZEND_AST_ZVAL { "cond" },
        },
        ZEND_AST_STMT_LIST {
            ZEND_AST_ECHO {
                ZEND_AST_ZVAL { "Cond is true\n" },
            },
        },
    },
}

文档强调两点:

  • 每个 AST 节点有类型、可以有子节点,同时记录其在源码中的原始位置,并可定义任意 flag(示例中为简洁而省略);
  • 与词法分析类似,解析器实现由 Bison 从文法规范生成,文法位于 Zend/zend_language_parser.y(该文件存在于仓库中,Bison 文法文件),且其语法"相当平易近人"。

3.3 编译(Compilation):AST → 虚拟指令

计算机只懂机器码——一组简单、近乎原子的指令(加两个数、从内存加载数据、条件跳转等)。但 PHP 不直接执行机器码,而是运行在一个虚拟机(VM)上:一台"用软件实现的、买不到的机器"。因此解释器可以自由发明指令——有的指令接近真实 CPU 指令集(如两数相加),有的则相当高层(如按名字加载对象属性)。

编译器的工作就是读取 AST、翻译为虚拟机的操作码(opcodes),其代码位于 Zend/zend_compile.c:本质上遍历 AST 节点,逐节点生成指令序列。上面那个 AST 对应的操作码"出人意料地紧凑":

0000 JMPZ CV0($cond) 0002
0001 ECHO string("Cond is true\n")
0002 RETURN int(1)

3.4 解释(Interpretation):三条地址码的虚拟机执行

操作码最终由解释器读取并执行。PHP 的指令采用**三条地址码(three-address code)**形式:每条指令至多有一个结果值、两个操作数——绝大多数现代 CPU 也采用这种格式。PHP 中结果与操作数都是 zval(文档在原文中以相对路径 ../core/data-structures/zval 引用,对应仓库文件 docs/source/core/data-structures/zval.rst)。

关键实现文件(均在仓库中核实存在):

回到示例逐条执行:

  1. 从顶端 JMPZ 开始:若第一个操作数含"falsy"值,则跳转到第二个操作数编码的位置(0002);若为 truthy,则顺序执行下一条;
  2. ECHO 打印其第一个操作数;
  3. RETURN 终止当前函数。

由此可看出:解释器只在 $cond 为 truthy 时才执行 echo,否则跳过——这就是 PHP "从根本上"的工作方式。文档也坦承跳过了大量细节,VM 的完整复杂性会在专门的虚拟机章节中讨论(当前仍标记为 todo)。

3.5 Opcache:缓存与优化

每次请求都完整跑一遍上述管线代价高昂——但其实没有必要。可以把 opcodes 在请求之间缓存在内存中,从而跳过除执行阶段外的所有阶段,这正是 opcache 扩展的工作,它位于 ext/opcache/ 目录。

opcache 在缓存前还会对 opcodes 做优化:由于 opcache 预期会被复用很多次,花额外时间简化指令以提升执行期收益是值得的。优化器位于 Zend/Optimizer/ 目录,从源码结构看,其实现了多个具体优化 pass,包括 block_pass.cdce.c(死代码消除)、nop_removal.cescape_analysis.ccompact_literals.ccompact_vars.c 等。

JIT:opcache 还实现了即时编译器(just-in-time compiler),把虚拟 PHP 操作码转译为真正的机器指令,并利用运行时获得的信息。JIT 是极为复杂的软件,文档自认"只能触及皮毛";其代码位于 ext/opcache/jit/,包含 zend_jit.czend_jit_ir.czend_jit_trace.cir/tls/ 子目录。

四、核心数据结构:解释器的"内存模型"

文档的 Core 分区(core/data-structures/index.rst)声明"本节概览 php-src 使用的核心数据结构",并列出四篇子文档。结合源码可将其落点一一对应:

五、如何构建本地文档

docs/README.md 给出了完整的文档构建流程,这是入门 php-src 内部文档最直接的实操入口。前提条件是 Python 3 与 pip

cd docs
# 推荐:创建并激活 Python 虚拟环境
pip install --upgrade pip
pip install -r requirements.txt
make html

构建产物在 ./build/html/index.html,用浏览器打开即可。

5.1 依赖与构建细节

docs/requirements.txt 可确认文档只依赖四个组件:Sphinxsphinx-designsphinxawesome-themerstfmt

docs/Makefile 的实现值得注意:html 目标依赖 preflight,而 preflight 会在构建前对所有 *.rst 文件先执行 rstfmt -w 100(行宽 100)做格式预检/整理,再调用 sphinx-build -M html source build。Makefile 还提供了一个 check-formatting 目标(rstfmt -w 100 --check source)用于检查格式。

5.2 Sphinx 主题与扩展配置

docs/source/conf.py 展示了文档站的完整构建配置:

  • 主题sphinxawesome_theme,并通过 ThemeOptions 启用上一页/下一页导航(show_prev_next=True)、在页头注入指向 php 官方仓库的 GitHub 图标;
  • 扩展sphinx_design(用于设计类指令)与 sphinx.ext.autosectionlabel(支持按标题锚点直接引用小节);
  • 高亮定制:为 phpphp-annotations 两个语言标签注册了 PhpLexer(startinline=True),使 <?php 之后的内联代码也能正确高亮;
  • 样式微调:通过 docs/source/_static/css/code-no-font-ligatures.css 禁用代码字体连字,避免 C/PHP 源码中 ilj 等字符粘连影响阅读。

5.3 代码格式规范

文档统一用 rstfmt 工具格式化(见 docs/README.md):

rstfmt -w 100 source

README 同时如实说明该工具"并不完美"——遇到自定义 directive 时会失效,未来可能切换到 fork 或其他工具。

六、测试体系:编写与运行 phpt 测试

首页 toctree 的 Miscellaneous 分区把"编写测试"和"运行测试"作为与架构同等重要的主题,这反映出 php-src 对质量保障的态度。以下要点分别继承自 writing-tests.rstrunning-tests.rst,且均可在仓库中找到对应实体(如 run-tests.phptests/Zend/tests/ext/standard/tests/)。

6.1 phpt 测试是什么

  • phpt 测试是 PHP 内部与 QA 团队使用的脚本:既用于验证新发行版保留了旧版本的全部能力,也用于在当前版本中定位 bug;
  • 技能门槛极低——"只要会写 PHP 就能写测试":只需对 PHP 语言的基本理解、一个文本编辑器、以及获取代码运行结果的方式;
  • 运行机制:run-tests.php 读取 phpt 文件的各部分,生成并执行一个 .php 文件,再把实际输出与 phpt 文件中的期望输出比对,一致即通过;
  • 测试理念是"尝试弄坏被测函数":不仅测正常参数,还要测边界情况,故意触发错误是被允许甚至鼓励的。

6.2 命名规范与规模约束

类型 命名约定 示例
bug 测试 bug<bugid>.phpt bug17123.phpt
函数基础行为 <functionname>_basic.phpt dba_open_basic.phpt
函数错误行为 <functionname>_error.phpt dba_open_error.phpt
行为变体 <functionname>_variation.phpt dba_open_variation.phpt
扩展通用测试 <extname><no>.phpt dba_003.phpt

规模原则:小,越小越好,一条经验法则是输出不超过 10 行——一旦某处改动打破了测试,你能快速定位问题,而不是在千行输出里翻找;确实无法压缩时,用注释组织输出。

6.3 最小 phpt 结构

一个测试最少需要 --TEST----FILE----EXPECT--(或 --EXPECTF--)三个 section。文档给出的最小示例(对应 ext/standard/tests/strings/strtr.phpt 一类测试):

--TEST--
strtr() functionbasic test for strtr()
--FILE--
<?php
$trans = array("hello"=>"hi", "hi"=>"hello", "a"=>"A", "world"=>"planet");
var_dump(strtr("# hi all, I said hello world! #", $trans));
?>
--EXPECT--
string(32) "# hello All, I sAid hi planet! #"

各 section 的语义:--TEST-- 是单行标题;--FILE-- 是生成 .php 文件的主体(别忘开闭 <?php 标签);--EXPECT-- 是比对基准;建议用 var_dump() 产生输出以保证可诊断性。

6.4 常用 Section 一览

文档的 "PHPT Sections" 参考部分对每个 section 给出了描述、是否必需与格式说明,完整清单包括:

  • --TEST--(必需,单行纯文本标题);
  • --DESCRIPTION--(可选,多行补充说明,测试二进制完全忽略);
  • --CREDITS--(可选,致谢贡献者/TestFest 活动;新测试不建议再用于署名,Git 已准确跟踪);
  • --SKIPIF--(可选,PHP 代码段:输出以 skip 开头则跳过;以 xfail 开头标记为预期失败(PHP 7.2.0 起支持);以 flaky 开头标记为不稳定测试(PHP 8.2.25 / 8.3.13 起支持);推荐把公共条件放进 skipif.inc 复用);
  • --CONFLICTS--(可选,并行测试冲突键;同名 CONFLICTS 文件可替代;冲突于 all 的测试独占执行);
  • --WHITESPACE_SENSITIVE--(可选,声明测试不应被自动格式化改动,PHP 7.4.3 起);
  • --CAPTURE_STDIO--(可选,指定比对 STDIN/STDOUT/STDERR 中哪些流;省略时三者全部启用);
  • --EXTENSIONS--(可选,声明需要加载的共享扩展;扩展缺失时尝试查找共享模块,找不到则跳过);
  • --POST-- / --POST_RAW-- / --PUT-- / --GET-- / --COOKIE-- / --GZIP_POST-- / --DEFLATE_POST--(可选,强制使用 CGI 二进制而非 CLI,分别模拟表单数据、原始 POST 数据(可自定义 Content-Type)、PUT 数据、GET 参数、HTTP Cookie,以及对 POST 数据做 gzencode() / gzcompress() 编码);
  • --STDIN--(可选,向测试脚本标准输入喂数据);
  • 以及期望输出变体 --EXPECTF--(模式匹配:%s 任意字符串、%a 至少一个任意字符、%i 整数、%d 纯数字、%f 浮点数、%c 单字符、%x 十六进制、%w 任意空白、%e 目录分隔符)和 --EXPECTREGEX--(正则比对,注意需转义 string\(18\) 这类会被当成正则的字符)。

--CLEAN-- 清理机制:测试中创建的文件/目录应在结束时删除。关键点在于 --CLEAN-- 代码与 --FILE-- 代码独立执行(分别跑在 mytest.phpmytest.clean.php 中),因此文件路径不能依赖 FILE 段的变量,而应使用 __DIR__ 拼接,例如:

--FILE--
<?php
$temp_filename = __DIR__."/fred.tmp";
$fp = fopen($temp_filename, "w");
fwrite($fp, "Hello Boys!\n");
fclose($fp);
?>
--CLEAN--
<?php
$temp_filename = __DIR__."/fred.tmp";
unlink($temp_filename);
?>

文档还给出了实践细节:临时文件取能表明用途的扩展名(如 .tmp)、避免与 .inc/.php 等已占用扩展名冲突、文件名与测试名关联(mytest.phptmytest.tmp);调试时可用 run-tests.php--keep 选项阻止中间文件删除。

6.5 失败分析与可移植性

make test 失败时,会在 phpt 同目录生成调试文件(以 foo.phpt 为例):foo.diff(期望与实际输出的差异)、foo.exp(期望输出)、foo.log(日志)、foo.out(实际输出)、foo.php(实际执行的 PHP 代码)、foo.sh(按失败时的方式重跑测试的可执行脚本)。

可移植性方面,文档给出了一组针对平台的 --SKIPIF-- 惯用法:

--SKIPIF--
<?php
if (PHP_INT_SIZE != 4) die("skip this test is for 32bit platforms only");
?>

(64 位平台用 PHP_INT_SIZE != 8;仅 Windows 用 substr(PHP_OS, 0, 3) != 'WIN';仅 Linux 用 !stristr(PHP_OS, "Linux");Mac OS X Darwin 用 !stristr(PHP_OS, "Darwin")。)

日期相关测试必须在 FILE 段(而非 INI 段)中显式 date_default_timezone_set('UTC'),因为解析顺序是 date_default_timezone_set() -> TZ 环境变量 -> INI 设置 -> 系统设置,存在 TZ 环境变量时 INI 设置会被忽略。

6.6 运行测试:make test 与 run-tests.php

running-tests.rst 给出了运行层面全部关键信息:

基本用法:编译成功后 make test 会运行源码根目录下各 tests 目录中所有已启用功能与扩展的测试。make test 本质是执行源码根目录下的 run-tests.php(并行构建下不可用),等价于:

sapi/cli/php [-c /path/to/php.ini] run-tests.php [ext/foo/tests/GLOB]

可执行文件与 php.ini:手动运行 run-tests.php 时,可用环境变量 TEST_PHP_EXECUTABLE 显式指定被测 PHP 二进制,否则默认用刚编译出的 sapi/cli/php。注意执行 run-tests.php 的二进制与执行测试脚本的二进制可以不同,但混用不同版本可能报错。make test 会自动设置 CLI 与 CGI 可执行文件;部分测试(如 session)必须用 CGI SAPI 执行,因此要跑完整测试需带 CGI SAPI 构建 PHP。make test 使用的 php.ini 与安装后一致;测试被设计为与该文件无关,若发现受某设置影响的测试应反馈。

选择测试:不带参数时运行全部测试(从源码根及子目录中抽取所有名为 tests 的目录,评估每个 .phpt 的 SKIPIF 段决定是否执行,执行时把 FILE 段提取为同名 .php 文件);给参数或设置 TESTS 环境变量时,GLOB 由 shell 展开,所有 *.phpt 视为测试文件:

./sapi/cli/php run-tests.php ext/mbstring/*
./sapi/cli/php run-tests.php ext/mbstring/020.phpt
make test TESTS=ext/standard.
make test TESTS=tests/basic/001.phpt

测试运行器选项php run-tests.php -h 查看全部选项;选项可直接在命令行给出,也可通过 make testTEST_PHP_ARGS 传入:

php run-tests.php -j24
# 
TEST_PHP_ARGS="-j24" make test

其中 -j 用于并行执行,例如 php run-tests.php -j24 ext/date/*.phptTESTS 也可以透传选项给底层脚本,例如用 Valgrind 检查内存泄漏:make test TESTS="-m Zend/"make test TESTS=-h 可查看全部可透传选项。

结果与日志:失败时(以 ext/myext/tests/myext.phpt 为例)在该目录生成 myext.php(实际执行的测试文件)、myext.log(L,执行日志)、myext.exp(E,期望输出)、myext.out(O,实际输出)、myext.diff(D,两者差异)。文档明确指出"失败的测试永远是 bug——要么测试本身有毛病/没考虑环境因素,要么 PHP 有 bug";已知 bug 以 bug 号命名(测试名中 # 前缀、或文件名 bug12345.phpt,仓库中如 Zend/tests/ 下大量 bug*.phpt 即为佐证)。可用 TEST_PHP_LOG_FORMAT 环境变量控制生成哪些日志文件(字母对应上述括号标注,默认 LEOD.php 始终生成),TEST_PHP_DETAILED 开启详细测试信息。

自动化:设置 NO_INTERACTION=1 可禁用一切交互提示;REPORT_EXIT_STATUS=1 使 make test 在存在失败测试时返回非零退出码(run-tests.php 本身返回 1,而 make 的结果可能更高,故脚本中应判断"非零"而非具体值)。文档还收录了一个由 cron 驱动的 qa-test.sh 完整示例脚本,演示了 --disable-all --enable-cli --with-pcre 配置、NO_INTERACTION / REPORT_EXIT_STATUS 组合、以及失败时邮件通知的流程。

Windows 注意:Windows 上 make 对应 nmake,即需运行 nmake test

七、获取帮助

首页的 "How to get help?" 一节提醒:面对 php-src 这样庞大而复杂的项目,入门难免不知所措。绕开大量读码是不可能的,但向有经验的开发者提问能省下大量时间,而许多核心开发者乐于相助。文档列出的渠道包括 Discord 的 #php-internals 频道以及 StackOverflow 上的 PHP R11 聊天室;此外,编写测试相关的问题可以发给 php-qa 邮件列表(见 writing-tests.rst 中"友善且欢迎"的社区说明),提交测试前说明你在哪个 PHP 版本、哪些平台上验证过。

八、小结

docs/source/index.rst 作为 php-src 内部文档的总入口,其价值在于给出了一张完整的技术地图:以四阶段管线(tokenization → parsing → compilation → interpretation,对应 Zend/zend_language_scanner.lZend/zend_language_parser.yZend/zend_compile.cZend/zend_vm_def.h)理解解释器本体,以 Opcache(ext/opcache/)与 Optimizer(Zend/Optimizer/)、JIT(ext/opcache/jit/)理解性能层,以 zval/引用计数/zend_string/zend_constant 理解内存模型,以 phpt 测试体系(run-tests.php + 各 tests/ 目录)保障质量,再以 docs/ 下基于 Sphinx 的构建链(docs/Makefile + docs/requirements.txt + docs/source/conf.py)持续沉淀上述知识。文档虽自认"尚在进行中",但沿着这份骨架与仓库中已核实存在的源码文件对照阅读,足以支撑从"能读懂架构"到"能动手改代码、写测试"的完整路径。

登录后查看全文
热门项目推荐
相关项目推荐