diff --git a/docs/README.md b/docs/README.md index d67241ac..ad25ba86 100644 --- a/docs/README.md +++ b/docs/README.md @@ -25,6 +25,7 @@ - [重建 PHPX WASM 静态库](PHPX_WASM_BUILD.md):增量重编 `libphpx.a`、数值依赖重建与完整 SDK 重建边界。 - [核心重构计划](REFACTORING_PLAN.md) - [作用域管理设计](SCOPE_MANAGEMENT.md):`CallableScope`、`UserCodeScopeGuard` 与 `FakeScopeGuard` 的职责和使用边界。 +- [运行时初始化与关闭流程](RUNTIME_LIFECYCLE.html):PHP、PHPX、TypePHP 与项目四层生命周期,覆盖 bin/ext/lib、多模块及 WASM。 - [C++ 命名空间、前缀与符号 ABI](CPP_SYMBOL_NAMING.md):`typephp_`、`php::`、`typephp_` 与用户 callable `php_` 的职责边界和冲突规则。 - [Zend Object 创建与属性默认值初始化](OBJECT_CREATION.md):`gen_stub.php` 默认属性表、自定义 `create_object` 的触发条件、执行流程与性能边界。 - [Native Class Object 设计](NATIVE_CLASS_OBJECT.md) 与 [实现验收矩阵](NATIVE_CLASS_IMPLEMENTATION_AUDIT.md)。 diff --git a/docs/RUNTIME_LIFECYCLE.html b/docs/RUNTIME_LIFECYCLE.html new file mode 100644 index 00000000..437b1d9d --- /dev/null +++ b/docs/RUNTIME_LIFECYCLE.html @@ -0,0 +1,559 @@ + + + + + + TypePHP 运行时初始化与关闭流程 + + + +
+
+

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 多实例与多模块

+
    +
  • 同一个 Component 实例:生成的 adapter 只允许一个活动 runtime resource,并拒绝同一 resource 上的并发或重入调用。
  • +
  • 多个 Component 实例:各自拥有独立 linear memory 和 C/C++ 全局区,因此 ZendVM 状态互相隔离,可以由 Host 分别管理。
  • +
  • 同一个 WASM 内的扩展:PHP 扩展必须在 SDK/最终链接阶段静态纳入,不能像原生 PHP 一样在运行时加载多个 TypePHP 动态扩展。
  • +
  • 浏览器:应在 finally 中显式调用 runtime[Symbol.dispose]();直接终止 Worker 只会回收整个实例,不保证 shutdown callback 被执行。
  • +
  • Wasmtime:command 返回时自动关闭;component library 应显式 drop resource。两者与浏览器使用相同的 TypePHP runtime ABI。
  • +
+
+ +
+

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

+
+
从仍可执行用户代码,到最终释放 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. +
  3. PHPX 在项目 clean 之前:Native finalizer 属于用户代码,可能访问项目 globals;因此生成的 RSHUTDOWN 当前是 php::request_shutdown() 后接 module_clean()
  4. +
  5. 请求对象在 PHP 内存池之前释放:若 PHP request arena 已销毁,栈上或全局的 PHPX wrapper 再析构会变成悬空指针访问。
  6. +
  7. Embed 晚注册模块要提前移出 registry:删除操作会通过 Zend 的 module destructor 执行项目 MSHUTDOWN,同时规避 Embed 对 persistent strings 的重复释放问题。
  8. +
  9. 多 ext 共享 PHPX:第一个进入的 TypePHP RSHUTDOWN 完成共享 PHPX cleanup,后续调用幂等返回;每个项目自己的 module_clean() 仍各执行一次。
  10. +
+
+ +
+

八、函数名称辨析

+
+ + + + + + + + + + + + + +
符号定义层寿命职责
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. +
  3. 确认谁拥有 PHP runtime:外部 SAPI、生成 main、native host,还是 WIT resource。
  4. +
  5. 确认目标模块是启动前注册还是 php_embed_init() 后晚注册。
  6. +
  7. 为每个项目分别记录 MINIT/RINIT/RSHUTDOWN/MSHUTDOWN 次数,不要只看共享 PHPX 标志。
  8. +
  9. 崩溃若发生在 PHP memory manager shutdown,检查是否仍有项目 global、Closure、Object 或 PHPX wrapper 晚析构。
  10. +
+
+ +
+

十、代码依据

+
    +
  • compiler/src/Translator.php::genExtension():生成项目 module entry、MINIT/MSHUTDOWN/RINIT/RSHUTDOWN、module_init/module_clean
  • +
  • phpx/src/misc/typephp_main.cc:原生 bin/lib 与 WASI 共用的 Embed runtime owner、晚注册模块和 shutdown 顺序。
  • +
  • phpx/src/core/base.cc::php::request_init/request_shutdown:PHPX 请求级共享状态。
  • +
  • phpx/include/typephp_runtime.h:项目名隔离的 runtime/get-module ABI 宏。
  • +
  • compiler/src/Build/WasmInterfaceGenerator.php:WIT runtime resource、单实例约束、异常边界和 resource destructor。
  • +
  • projects/php-8.5.9/sapi/embed/php_embed.cphp_embed_init/shutdown 内部的 SAPI/module/request 调度。
  • +
  • projects/php-8.5.9/main/main.c::php_request_shutdown():shutdown function、析构、RSHUTDOWN、内存池销毁的标准顺序。
  • +
+
+ +
此页面是内部实现文档。流程变化时,应同时更新对应代码注释、生命周期测试和本图。
+
+ + diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 4b00fd36..ecf66078 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -5154,7 +5154,7 @@ class CompilerBase implements PropertyAccessContext // nativeClone do the same for lifecycle callbacks). Only // function-owned pointer slots must be registered here. if ($name !== 'this_' && !$this->hasArgument($name) && $this->hasLocalVar($name)) { - $rootSlots[] = 'reinterpret_cast(&' . $name . ')'; + $rootSlots[] = '&' . $name; } } if ($rootSlots !== []) { diff --git a/src/Translator.php b/src/Translator.php index c6429060..41732c65 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -1062,8 +1062,8 @@ CODE; continue; } if (isset($this->nativeGlobalObjects[$name])) { - $code .= 'php::nativeGcRegisterRequestRoot(reinterpret_cast(&' - . $this->escapeGlobalVar($name) . '));' . PHP_EOL; + $code .= 'php::nativeGcRegisterRequestRoot(&' + . $this->escapeGlobalVar($name) . ');' . PHP_EOL; continue; } $code .= 'php::initGlobal(' . $this->genCharPtr($name) . ', ' . $this->escapeGlobalVar($name) . ');' . PHP_EOL; diff --git a/tests/compiler/array/null-key-append.phpt b/tests/compiler/array/null-key-append.phpt new file mode 100644 index 00000000..e51d6d2c --- /dev/null +++ b/tests/compiler/array/null-key-append.phpt @@ -0,0 +1,30 @@ +--TEST-- +null array keys append while boolean keys use integer indexes +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + string(5) "first" + [1]=> + string(6) "second" +} +array(2) { + [0]=> + string(4) "zero" + [1]=> + string(3) "one" +} diff --git a/tests/compiler/string_method/str-offset-set.phpt b/tests/compiler/string_method/str-offset-set.phpt index 2d98aa05..6e2e5767 100644 --- a/tests/compiler/string_method/str-offset-set.phpt +++ b/tests/compiler/string_method/str-offset-set.phpt @@ -5,8 +5,10 @@ string offset set $s1 = "hello world"; $s2 = "php"; $s1[1] = $s2[1] = '_'; +$last = -1; +$s1[$last] = '!'; var_dump($s1, $s2); ?> --EXPECT-- -string(11) "h_llo world" +string(11) "h_llo worl!" string(3) "p_p"