首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >解码 TypePHP〡04. 告别解释执行!TypePHP 编译原理深度拆解

解码 TypePHP〡04. 告别解释执行!TypePHP 编译原理深度拆解

作者头像
Tinywan
发布2026-09-15 14:38:55
发布2026-09-15 14:38:55
1420
举报
文章被收录于专栏:开源技术小栈开源技术小栈

TypePHP 编译原理深度拆解

写在前面|前两篇装好了环境、编出了第一个二进制,但 tpc 对我们来说还是个黑盒:PHP 进去,二进制出来,中间发生了什么?

这篇拆盒。TypePHP 官方文档里有一篇《执行过程》,把编译器内部讲得相当透,我结合 --dry 参数带你看一遍完整流水线。

01. 全景:官方定义的四阶段流水线

官方文档把编译过程定义为四个阶段,全部离线完成:

代码语言:javascript
复制
┌───────────────────────────────────────┐
│ 输入: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++ 工具链。

  1. 预处理 prepare() :扫描、收集、排序所有源文件,构建完整的符号表
  2. 转换 convert() :把 PHP 翻译成等价的 C++ 源码
  3. 编译 compile() :交给 GCC/Clang/MSVC 生成目标文件
  4. 链接 build() :链接运行时库,产出可执行文件(ELF/PE)或动态库(.so/.dll)

这个分工是理解整个项目的一把钥匙:TypePHP 专注于"把 PHP 语义正确翻译成 C++"这件最难的事,优化和代码生成全部委托给几十年功力的 C++ 编译器。

02. 转换阶段:PHP 到 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 装箱拆箱。

映射规则大致是:

  • PHP 函数 → C++ 函数,用户函数之间是直接 C++ 调用(存在子类覆盖时编译器会尝试去虚化)
  • PHP 类 → C++ class,属性按固定偏移布局(第三篇说的 C Struct 式优化)
  • 内置函数 → 通过 ZendAPI 的 zend_call_function() 直调底层 C 函数指针
  • 无法静态确定的部分 → 保留运行时动态能力

想看真实产物?用 --dry

代码语言:javascript
复制
tpc hello.php --dry

它只生成 C++ 文件,不调用 C++ 编译器和链接器。去 build 目录里翻翻,就能看到你的 PHP 被翻译成的 C++ 源码。第一次看会有点震撼:一个 echo 语句背后是 phpx 运行时的输出调用,一个类型明确的循环就是干净的 C++ while。

指定生成目录也行:

代码语言:javascript
复制
tpc project.yml --dry --build-dir /tmp/typephp-build

学编译原理、排查"为什么这段代码没被优化",都靠这个参数。

03. 第三阶段:C++ 编译

翻译出来的 C++ 交给系统编译器。这里有一组关键参数:

优化级别-O0 到 -O3,对应 tpc 的 --debug(-O0)到 -O2/-O3。跑分用的就是 -O3。日常开发用 --debug 编译快,发布用 -O2 起步。

并行编译-j8 开多任务,大项目提速明显:

代码语言:javascript
复制
tpc project.yml -O2 -j8

编译器选择。GCC、Clang、MSVC 都行,project.yml 里可以指定:

代码语言:javascript
复制
cpp-compiler: clang++

C++ 标准。默认 c++20,可配置:

代码语言:javascript
复制
cxx-std: c++20

Hot/Cold 注解就是在这一阶段生效的——它们被翻译成 __attribute__((hot/cold)),影响 C++ 编译器的优化决策。

04. 第四阶段:链接

C++ 目标文件和两个核心库链接:

  • libphp.so:PHP Embed,提供内置函数、ZendVM 兜底执行能力
  • libphpx.so:TypePHP 运行时,提供原生类型、容器、高精度数学等基础设施

这就是为什么部署要带 libphpx、为什么安装时 ldd 检查那么重要。链接完,产物就是最终的原生二进制:ELF(Linux)、Mach-O(macOS)或 PE(Windows)。

05. 运行时:三种执行模式协同

很多人以为编译后 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),收益是兼容性——你不需要把代码改成"编译器喜欢的样子"才能跑起来,只是跑得快慢的区别。

06. 执行入口的约定

还记得 main() 约定吗?现在能解释清楚了:原生程序不经过 PHP 的脚本加载流程,需要一个明确的 C 风格入口。TypePHP 把你的全局 main() 翻译成二进制的真正入口点,argc/argv 可以直接拿到命令行参数:

代码语言:javascript
复制
function main(int $argc, array $argv): void
{
    echo "参数数量: $argc\n";
}

这也是为什么 TypePHP 特别适合写 CLI 工具——入口模型和 C 完全一致,启动没有 PHP 运行时的引导开销。

07. 缓存与增量编译

tpc 有编译缓存,改一个文件不会全量重编。需要强制重编时:

代码语言:javascript
复制
tpc project.yml --force

注意 --profile 参数会强制重编 main.cc,因为要注入 ProfilerStart/ProfilerStop 调用(第二篇讲过)。

08. 小结

  • 四段流水线:prepare 预处理 → convert 翻译 C++ → compile 系统编译器 → build 链接,产出原生二进制
  • TypePHP 只管翻译,优化交给 GCC/Clang/MSVC
  • --dry 是学习利器,能看到翻译出的真实 C++ 代码
  • 编译与解释双形态共存,ZendVM 负责兜底,兼容性优先
  • main() 是原生入口,CLI 场景启动零引导开销

下一篇进入类型系统:TypePHP 只给了三种原生类型,它是怎么用这么少的类型撑起 PHP 的动态世界的?use native_types 背后的完整规则一次讲清。

参考资料

  1. 执行过程:https://www.swoole.com/aot/docs/execution
  2. 编译项目:https://www.swoole.com/aot/docs/compile
  3. Printer:https://www.swoole.com/aot/docs/printer
  4. 命令行参数:https://www.swoole.com/aot/docs/options

—— 如果这篇对你有帮助 ——

点赞 · 在看 · 转发

关注「开源技术小栈」,第一时间见证 TypePHP 的每一次关键进化

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-08-21,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • TypePHP 编译原理深度拆解
    • 01. 全景:官方定义的四阶段流水线
    • 02. 转换阶段:PHP 到 C++ 的映射规则
    • 03. 第三阶段:C++ 编译
    • 04. 第四阶段:链接
    • 05. 运行时:三种执行模式协同
    • 06. 执行入口的约定
    • 07. 缓存与增量编译
    • 08. 小结
    • 参考资料
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档