
写在前面|前两篇装好了环境、编出了第一个二进制,但 tpc 对我们来说还是个黑盒:PHP 进去,二进制出来,中间发生了什么?
这篇拆盒。TypePHP 官方文档里有一篇《执行过程》,把编译器内部讲得相当透,我结合 --dry 参数带你看一遍完整流水线。
官方文档把编译过程定义为四个阶段,全部离线完成:
┌───────────────────────────────────────┐
│ 输入:PHP 源码(.php / project.yml) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ① prepare() 预处理 │
│ 扫描、收集、排序全部源文件 │
│ 产出:完整符号表 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ② convert() 转换 │
│ PHP AST → 等价的 C++ 源码 │
│ (tpc --dry 可看到这一层产物) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ③ compile() 编译 │
│ 交给 GCC / Clang / MSVC │
│ 产出:目标文件(-O2/-O3 在此生效) │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ ④ build() 链接 │
│ 链接 libphp + libphpx 运行时库 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ 原生二进制:ELF / Mach-O / PE │
└───────────────────────────────────────┘
注意这条流水线的分工边界:TypePHP 自己只实现 ①② 两个阶段,③④ 完全交给系统里的 C++ 工具链。
这个分工是理解整个项目的一把钥匙:TypePHP 专注于"把 PHP 语义正确翻译成 C++"这件最难的事,优化和代码生成全部委托给几十年功力的 C++ 编译器。
这是最有意思的一步。TypePHP 把每个 PHP 函数、类翻译成 C++ 代码,类型处理分两个层次:
类型层次 | 启用方式 | C++ 类型 |
|---|---|---|
原生类型 | use native_types | php::Int/php::Float/php::Bool,即 int64_t/double/bool |
动态类型 | 默认(未声明) | php::Var(zval 包装)、php::Array、php::String、php::Object |
也就是说,同一个 int,开不开 native_types 是两种命运:开了就是裸的 int64_t 直接运算,不开就还是 zval 装箱拆箱。
映射规则大致是:
zend_call_function() 直调底层 C 函数指针想看真实产物?用 --dry:
tpc hello.php --dry
它只生成 C++ 文件,不调用 C++ 编译器和链接器。去 build 目录里翻翻,就能看到你的 PHP 被翻译成的 C++ 源码。第一次看会有点震撼:一个 echo 语句背后是 phpx 运行时的输出调用,一个类型明确的循环就是干净的 C++ while。
指定生成目录也行:
tpc project.yml --dry --build-dir /tmp/typephp-build
学编译原理、排查"为什么这段代码没被优化",都靠这个参数。
翻译出来的 C++ 交给系统编译器。这里有一组关键参数:
优化级别。-O0 到 -O3,对应 tpc 的 --debug(-O0)到 -O2/-O3。跑分用的就是 -O3。日常开发用 --debug 编译快,发布用 -O2 起步。
并行编译。-j8 开多任务,大项目提速明显:
tpc project.yml -O2 -j8
编译器选择。GCC、Clang、MSVC 都行,project.yml 里可以指定:
cpp-compiler: clang++
C++ 标准。默认 c++20,可配置:
cxx-std: c++20
Hot/Cold 注解就是在这一阶段生效的——它们被翻译成 __attribute__((hot/cold)),影响 C++ 编译器的优化决策。
C++ 目标文件和两个核心库链接:
这就是为什么部署要带 libphpx、为什么安装时 ldd 检查那么重要。链接完,产物就是最终的原生二进制:ELF(Linux)、Mach-O(macOS)或 PE(Windows)。
很多人以为编译后 ZendVM 就退场了,其实不是。官方文档说得很清楚:TypePHP 程序运行在混合执行环境中,有三种模式:
模式 1:静态编译执行。AOT 编译过的用户代码,直接机器指令执行,无 zval 装箱、无 Opcode,接近原生 C++ 性能。
模式 2:ZendAPI 直接调用。PHP 内置函数(explode、preg_match)和扩展函数(json_decode、curl_init),通过 zend_call_function() 直调底层 C 函数指针,不生成 Opcode。开销只有函数指针查找和参数的 zval 包装。
模式 3:ZendVM 解释执行。include()/require()、eval()、create_function()、动态类定义,这些只能运行时走完整的"解析→编译→解释"管线,效率和标准 PHP 一样。
注意 include/require 也在模式 3 里——这点容易被忽略。所以官方给的最佳实践是:核心业务逻辑放静态编译文件,只把配置加载、路由分发这类必要场景留给动态加载。
还有个冷知识:TypePHP 定义的 Trait 只作为编译期 AST 模板存在,不注册到 ZendVM,所以动态代码里 use 不了 TypePHP 的 Trait。
这个设计的代价是每个二进制都带着 ZendVM(所以部署要 libphp),收益是兼容性——你不需要把代码改成"编译器喜欢的样子"才能跑起来,只是跑得快慢的区别。
还记得 main() 约定吗?现在能解释清楚了:原生程序不经过 PHP 的脚本加载流程,需要一个明确的 C 风格入口。TypePHP 把你的全局 main() 翻译成二进制的真正入口点,argc/argv 可以直接拿到命令行参数:
function main(int $argc, array $argv): void
{
echo "参数数量: $argc\n";
}
这也是为什么 TypePHP 特别适合写 CLI 工具——入口模型和 C 完全一致,启动没有 PHP 运行时的引导开销。
tpc 有编译缓存,改一个文件不会全量重编。需要强制重编时:
tpc project.yml --force
注意 --profile 参数会强制重编 main.cc,因为要注入 ProfilerStart/ProfilerStop 调用(第二篇讲过)。
--dry 是学习利器,能看到翻译出的真实 C++ 代码下一篇进入类型系统:TypePHP 只给了三种原生类型,它是怎么用这么少的类型撑起 PHP 的动态世界的?use native_types 背后的完整规则一次讲清。
—— 如果这篇对你有帮助 ——
点赞 · 在看 · 转发
关注「开源技术小栈」,第一时间见证 TypePHP 的每一次关键进化