TypePHP 运行时生命周期

PHP、PHPX、TypePHP 生成模块与具体项目的 init/shutdown 调用关系;覆盖原生 bin、ext、lib,多 TypePHP 模块,以及 WASI command / component。

依据当前实现整理 · 2026-08-22 · TypePHP 4e00799 · PHPX f0a67ba · PHP/WASI 4dafa96b

一、先分清四个层次

PHP / ZendVM

运行时所有者

php_embed_init()php_request_startup()php_embed_shutdown(),以及 MINIT/RINIT/RSHUTDOWN/MSHUTDOWN 的最终调度者。

PHPX

C++ 安全封装与共享状态

php::request_init() 初始化 Decimal、Native GC、Box 资源;php::request_shutdown() 执行 Native finalizer 并清理请求缓存。

TypePHP

生成的 Zend 模块

生成 zend_module_entry、项目专属 MINIT/MSHUTDOWN/RINIT/RSHUTDOWN,以及 typephp_<project>_runtime_* ABI。

Project

项目私有状态

全局变量、静态属性、数组常量、请求模板、Python module cache、项目符号 cache,以及 bin 模式的 main()

同名但不同层:PHPX 的文件内 module_init(zend_module_entry *) 表示“向 Zend 注册并启动一个模块”;生成在项目命名空间里的 static module_init() 表示“初始化项目请求级数据”。后者不是 MINIT。

生命周期嵌套关系

外层寿命包含内层寿命;内层必须先结束
PHP / SAPI lifetimeSINIT → module startup → module shutdown → SSHUTDOWN
TypePHP module lifetimeMINIT → MSHUTDOWN;每个项目模块各一份
PHP request lifetimerequest startup → shutdown callbacks/destructors → request shutdown
Project request stateRINIT/module_init → 执行 → RSHUTDOWN/module_clean

二、模式总览

模式PHP runtime 所有者项目模块何时注册项目 RINIT/RSHUTDOWN入口多模块结论
ext外部 PHP SAPI(CLI/FPM/Embed)PHP module startup 期间PHP 自动调度外部 PHP 脚本调用编译符号支持多个,但 PHP 符号不得冲突
bin生成的 C++ main()php_embed_init() 完成后才注册自身项目由 PHPX 手动补调RINIT 中 php::eval(... main())可再加载多个常规 ext
lib宿主显式调用项目 runtime ABI与 bin 相同,属于晚注册模块自身项目由 PHPX 手动补调宿主调用导出函数,无生成 main可加载多个库;同进程只能有一个活动 Zend runtime
WASI commandWASM _start/main与原生 bin 同构手动补调自身项目执行 TypePHP main()单实例、静态链接;无动态 ext
WASI component libraryWIT runtime resourcecreate-runtimeresource 创建/析构负责#[WasmExport] 方法每个 Component 实例仅一个活动 resource

三、EXT:PHP 自动管理多个 TypePHP 模块

假设 PHP 同时加载 typephp_app_atypephp_app_b。两个模块都在 PHP 收集 request handlers 之前注册,因此走标准 Zend 模块生命周期。

进程启动与一次 request
Module startup
PHP module startup读取 extension 配置,注册模块
A::MINIT注册 A 的类、函数、属性元数据
B::MINIT注册 B;共享 hook 采用计数
Request startup
php_request_startupzend_activate_modules()
A::RINITphp::request_init() 首次真正初始化;A module_init()
B::RINITPHPX init 幂等返回;B module_init()
Request shutdown
shutdown callbacks先执行注册的 shutdown function 与对象析构
B::RSHUTDOWN首次 php::request_shutdown() 清共享 PHPX 状态;再清 B
A::RSHUTDOWNPHPX shutdown 幂等返回;再清 A
Module shutdown
B::MSHUTDOWN清 B persistent cache,共享 hook 计数减一
A::MSHUTDOWN最后一个模块恢复 Reflection handler、清 FiberGenerator 借用指针
PHP module shutdownZend/SAPI 继续退出

多 ext 时哪些状态共享,哪些隔离

App A共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook私有:A globals/cache
App B共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook私有:B globals/cache
App C共享:同一 PHPX request、Native GC、Box 类型、Reflection/Fiber hook私有:C globals/cache
隔离保证:生成的 C++ 数据表位于 typephp_<project> 命名空间,runtime/get-module 符号也带项目名。多个 ext 不再因为 PHPX misc 或通用表名发生原生符号冲突。
仍然禁止:两个项目向 ZendVM 注册相同的 PHP namespace/class/function 组合。C++ 符号隔离不能消除 PHP function table/class table 的语义冲突。

四、BIN:自身项目晚注册,额外 EXT 正常注册

php_embed_init() 内部已经完成 SAPI startup、PHP module startup 和 PHP request startup。此后 bin 才取得自身的 zend_module_entry 并注册,所以自身项目不在 PHP 预先收集的 RINIT/RSHUTDOWN handler 列表中。

bin owner + ext A + ext B
C++ main()调用 typephp_<bin>_runtime_init(argc, argv)
php_embed_init()SAPI/PHP 启动;ext A/B 自动完成 MINIT,然后在 request startup 自动完成 RINIT
注册 bin 自身项目模块zend_register_module_ex() + zend_startup_module_ex() → bin::MINIT
手动 bin::RINITphp::request_init()(通常已被 ext 初始化)→ 项目 module_init()
php::eval(... main())开始执行 TypePHP 项目;所有模块共享这一 Zend request
shutdown function + __destruct项目请求状态仍然存活,先让用户清理代码执行完
手动 bin::RSHUTDOWNPHPX request shutdown → bin 项目 module_clean
从 module_registry 删除 bin触发 bin::MSHUTDOWN;避免 Embed 退出时 persistent string 重复释放
php_embed_shutdown()PHP request shutdown → ext B/A RSHUTDOWN → ext B/A MSHUTDOWN → SAPI shutdown
为什么不能让 PHP 自动调用 bin 自身的 RSHUTDOWN?PHP 在 php_embed_init() 期间已经收集完 module handler 数组;bin 模块注册得更晚,不在数组里。不手动补调会遗漏项目全局变量、Native roots、请求数组模板和缓存清理。

五、LIB:生命周期由宿主显式包围

lib 与 bin 使用同一份 PHPX Embed 实现,但定义 TYPEPHP_NO_MAIN,不会生成 C++ main()。宿主必须调用项目名隔离的 ABI。

typephp_demo_runtime_init(argc, argv);
// 调用 demo 导出的 TypePHP 函数
typephp_demo_runtime_shutdown();
单个活动 lib runtime
Hostdlopen/LoadLibrary 只装载代码
runtime_init真正启动 PHP Embed + 项目 MINIT/RINIT
Exports所有调用共享一个 PHP request
runtime_shutdown项目 RSHUTDOWN/MSHUTDOWN + PHP Embed shutdown
Unload运行时结束后才可卸载动态库

同一宿主加载多个 TypePHP lib

操作结论原因
只加载 A、B 的动态库,不调用 init可以导出 runtime/get-module 符号带项目名;装载代码不等于启动 ZendVM。
init(A) → use(A) → shutdown(A) → init(B)可串行前一 runtime 必须完整退出后,下一库才可重新启动进程级 PHP Embed 状态。
init(A) → init(B),两个同时活动不支持ZendVM/Embed 是进程级状态;每个 lib 的局部 runtime_started 无法阻止另一 lib 再次调用 php_embed_init()
多个项目需要同时工作一个 owner使用一个 bin/lib 拥有 runtime,其余能力作为启动前加载的 ext,或合并为同一个 TypePHP 项目。
关键约束:项目名隔离解决的是 C++ ABI 符号冲突,不会把一个进程切成多个 ZendVM。多个 native lib 的活动区间不得重叠;NTS 下也不得并发或重入调用同一 runtime。

六、WASM:相同内核,两种 Host 入口

WASI 使用静态链接的 PHP、PHPX 和 TypePHP 生成代码。浏览器与 Wasmtime 的 Host API 不同,但 PHP/ZendVM 生命周期相同。当前不支持在运行时动态加载 .so/.dll 形式的 TypePHP ext。

两个维度不要混淆:mode: command/library 决定生命周期入口(生成 main,或由 runtime resource 管理);wasm: component/browser 决定产物与 Host 适配方式。它们是正交配置,浏览器构建并不自动等于 library 模式。
WASI command

_start/main 自动包围

Wasmtime / WASI Host实例化 command 并调用 _start
runtime_initWASI Embed + 项目 MINIT/RINIT
main()与原生 bin 相同的入口语义
runtime_shutdown完整执行请求、模块与 SAPI 关闭
WASI component library

由 WIT resource 包围

Instantiate component只建立 WASM 实例,尚未启动 ZendVM
create-runtime调用项目 runtime_init,返回 resource
#[WasmExport]多次调用共享同一 request;异常映射为 WIT result
drop/dispose resource调用项目 runtime_shutdown

WASM 多实例与多模块

七、为什么关闭顺序不能随意调整

从仍可执行用户代码,到最终释放 PHP 内存池
shutdown functions允许访问项目 globals
__destruct()允许回调 TypePHP 方法
PHPX request shutdownNative GC finalizer 仍可访问项目状态
project module_clean释放 globals、数组模板和请求 cache
MSHUTDOWN清 persistent pointer cache / shared hook
PHP memory manager最后销毁 request arena 与 interned strings
  1. 用户清理代码在前:shutdown callback 和对象析构可能继续访问 TypePHP 全局变量、调用动态函数,不能在它们之前销毁项目状态。
  2. PHPX 在项目 clean 之前:Native finalizer 属于用户代码,可能访问项目 globals;因此生成的 RSHUTDOWN 当前是 php::request_shutdown() 后接 module_clean()
  3. 请求对象在 PHP 内存池之前释放:若 PHP request arena 已销毁,栈上或全局的 PHPX wrapper 再析构会变成悬空指针访问。
  4. Embed 晚注册模块要提前移出 registry:删除操作会通过 Zend 的 module destructor 执行项目 MSHUTDOWN,同时规避 Embed 对 persistent strings 的重复释放问题。
  5. 多 ext 共享 PHPX:第一个进入的 TypePHP RSHUTDOWN 完成共享 PHPX cleanup,后续调用幂等返回;每个项目自己的 module_clean() 仍各执行一次。

八、函数名称辨析

符号定义层寿命职责
php_embed_init/shutdownPHP Embed SAPI整个 runtime启动/关闭 SAPI、PHP modules 和一个 PHP request。
PHP_MINIT_FUNCTION(typephp_x)TypePHP 生成模块module注册项目类/函数/属性元数据;安装共享 TypePHP handler。
PHP_RINIT_FUNCTION(typephp_x)TypePHP 生成模块request调用 PHPX request init,再初始化项目请求状态;bin 额外执行 main。
php::request_init/shutdownPHPX每线程/每 request 共享管理 Decimal、Native GC、Box 和动态调用缓存;对多模块调用幂等。
static module_init()生成的项目命名空间request初始化项目 globals、静态属性、数组常量、请求模板和 roots。
static module_clean()生成的项目命名空间request清项目 globals/cache,并将 user-code symbol pointer 表归零。
module_init(zend_module_entry *)PHPX Embed gluemodule调用 Zend API 注册晚到的 bin/lib 项目模块并触发其 MINIT。
typephp_<project>_runtime_init/shutdownTypePHP 稳定 ABI,PHPX 实现runtimebin main、native lib host、WIT adapter 共同使用的外层入口。
php_<project>_embed_get_moduleTypePHP 生成模块module返回该项目唯一的 zend_module_entry;项目名避免多模块 C++ 符号冲突。

九、实现与维护规则

必须保持

  • 每个项目的 MINIT/MSHUTDOWN/RINIT/RSHUTDOWN 恰好各执行一次。
  • 所有可执行用户代码发生在项目请求状态和 PHP 内存池仍有效时。
  • 请求级指针在 RSHUTDOWN 清空;module persistent cache 在 MSHUTDOWN 清空。
  • 共享 hook 使用安装计数;项目私有表使用项目 namespace/static storage。
  • WASM resource 必须显式释放,不依赖 Host GC 的最终时间。

禁止出现

  • 在同一进程同时启动两个 native lib 所拥有的 PHP Embed runtime。
  • php_embed_shutdown() 后析构持有 request zval 的 C++ 对象。
  • 让晚注册的 bin/lib 项目依赖 PHP 自动 RINIT/RSHUTDOWN。
  • 在多 ext 中使用未加项目名的全局 C++ 数据表或 runtime ABI。
  • 误把项目 module_init() 当作 Zend MINIT,或在 MINIT 中创建请求 zval。

排查顺序

  1. 先确认当前是 binextlib、WASI command 还是 component library。
  2. 确认谁拥有 PHP runtime:外部 SAPI、生成 main、native host,还是 WIT resource。
  3. 确认目标模块是启动前注册还是 php_embed_init() 后晚注册。
  4. 为每个项目分别记录 MINIT/RINIT/RSHUTDOWN/MSHUTDOWN 次数,不要只看共享 PHPX 标志。
  5. 崩溃若发生在 PHP memory manager shutdown,检查是否仍有项目 global、Closure、Object 或 PHPX wrapper 晚析构。

十、代码依据