10 KiB
TypePHP Python 互调用分阶段实施计划
本计划以
python/design.md为规范。每个阶段严格执行:先增加 PHPUnit/PHPT/pytest 测试并确认失败,再实现,再运行相关测试和完整回归。
阶段 1:Python use 与 module binding
目标是完成最小可运行闭环,不实现运算符和通用转换:
- 识别
use python\module、根名称大小写不敏感和 Python 后续名称大小写敏感。 - 建立文件级 Python module alias 表,并与普通 class/function/constant use 检查冲突。
- 仅在出现
module::$attr或module::func()时分配 module ID。 - 生成与
funcMap同类的pythonModuleMap、lazy getter 和 request-clean 代码。 - 使用 Zend class/function map 动态调用
PyCore::import();不 include、link 或检测 phpy。 - 使用 Zend object API 读取 module 属性及调用 module callable。
- phpy 未加载时在首次实际使用处抛出 PHP
Error;未使用的 Python use 不触发错误。
测试顺序:PHPUnit 代码生成与诊断测试 → PHPT 运行时测试 → 现有 compiler 回归。
阶段 2:builtins、构造语法糖与静态类型
python\name()通过 phpy Zend Facade 动态调用:显式PyCore方法直接复用,其他名称经 Pythonbuiltinsmodule lookup。python\list/dict/tuple/set/str/object()映射既有 phpy Zend 类或方法。new PyList()与python\list()等写法获得相同的逻辑静态类型。- Python 调用结果保持
PyObject或已知 phpy 子类,关闭 TypePHP 路径的隐式 scalar conversion。 - 缺少 phpy、builtin 不存在、参数错误和异常映射测试。
实现状态:已完成。TypePHP 在首次实际执行 Python 表达式时延迟启用 phpy 的 return_as_object,仅声明未使用的 Python 符号仍不触发运行时依赖。
阶段 3:参数转换与显式结果转换
- TypePHP 参数从左到右求值后自动转换为 Python 值。
- 标量、数组、空数组、嵌套容器及 TypePHP callable 转换。
- 通过
$py->toPlainValue()或兼容入口python\scalar($py)离开 Python 对象规则;需要确定原生类型时继续使用普通 TypePHP 转换,例如$py->toPlainValue()->toInt()。 - 深拷贝、递归容器、溢出、Unicode/bytes 和异常路径测试。
- review 并重构 phpy 转换策略,移除影响同步重入的全局临时转换状态。
实现状态:核心边界已完成。TypePHP 参数严格从左到右求值,支持标量、空数组、嵌套 list/dict 与 callable;toPlainValue() 是推荐的链式显式转换入口,python\scalar() 保留为等价兼容入口。PHPX 在运行时确认 php::Var 确实持有 PyObject,再通过 Zend Facade 调用 PyCore::scalar();不依赖 phpy C++ 符号。phpy 已移除进程级转换函数指针,改为局部有状态转换器、RAII 递归保护和 128 层深度限制,并覆盖无效 UTF-8、PHP 自引用数组及 Python 循环容器错误路径。Python 大整数与 bytes 的最终语言映射仍保留在本阶段后续工作中。
阶段 4:运算符
- 将运算符改写为 Python 标准库
operatormodule 的动态调用。 - 混合操作数先转换为
PyObject。 - 严格保证从左到右、各求值一次。
- 使用
operator.is_/is_not/truth实现 identity 和 truthiness,使用iadd/isub/...实现 compound assignment。 - 验证
operator自动处理NotImplemented、reflected dunder 和子类优先级。 - 对照 phpy opcode-handler 行为,修复
/错误映射 floor division 等既有问题。
实现状态:已完成。二元算术、位运算、比较、===/!==、一元运算、条件真假值、短路逻辑和复合赋值均通过隐式 operator module binding 执行;/ 使用 truediv。混合 TypePHP 操作数由 phpy 在调用边界转换,结果继续保持 PyObject,比较和真假值结果显式收敛为 TypePHP bool。属性和下标左值由阶段 5 的动态写入协议完成回写。
阶段 5:完整对象协议
- Python 对象属性读写和删除。
- 下标读写、删除、
isset()。 - iterator/foreach。
- Python callable 和 TypePHP callable proxy 的同步重入。
- keyword argument、argument unpacking 和错误语义。
实现状态:已完成。Python proxy 的动态属性、未知方法、下标、删除、isset()、foreach 和 callable 均复用 phpy 的 Zend object protocol,不生成 phpy C++ 符号。方法、属性、下标和 callable 结果会继续传播为 PyObject,支持链式访问和后续 Python 运算。named argument 与 unpacking 复用统一调用参数管线并保持从左到右求值;属性和下标复合赋值使用 operator.i*() 的返回对象回写原左值。
phpy 同步完成了对象协议加固:__set() 转换引用释放、__unset()、list/tuple 负索引、list 删除、缺失键与 Python None 的 isset() 语义、删除和 contains 状态检查,以及 iterator/count 异常传播。相关 BUG 均由 phpy PHPUnit 与 TypePHP PHPT 独立覆盖。
阶段 6:phpy 稳定性与性能收尾
- CPython/ZendVM 生命周期、GIL、owned/borrowed/stolen reference 全量审计。
- Python/Zend 异常状态和 traceback 审计。
- 跨 VM 引用环、析构和异常注入测试。
- ASan/UBSan、PHP leak report、Python debug build 和压力测试。
- 基准测试动态 Zend call、module map、参数转换和
operatormodule 调用;只优化被数据证明的热点。 - 完整 PHPUnit、pytest、PHPT 和现有 TypePHP compiler 回归。
实现状态:进行中。第一轮 CPython 失败路径审计已覆盖通用对象、list、dict、tuple、set 的构造和下标写入,以及 sequence/set 的 contains()。PHP 到 Python 的 key/value 转换失败现在会立即映射为 PyError,所有已取得的新引用均由作用域守卫释放;构造失败不再留下未处理的 CPython error indicator,contains() 的 -1 错误结果也不再被误判为 true。无效 UTF-8、unhashable set member、失败后容器仍可继续使用等路径已有 phpy PHPUnit 回归测试。
第二轮审计覆盖 module import、异常转换、callable 检查和显式 iterator API。PyImport_ImportModule()、PyErr_Fetch() 和 PyIter_Next() 转移给调用方的新引用现在都会在 Zend wrapper 取得独立引用后统一释放;重复 import、Python 异常或显式 iterator next 不再持续增加引用计数。调用非 callable 的 Python 属性或 PyObject 会稳定抛出 PyError(TypeError),不再因为 PyCallable_Check() 未设置 error indicator 而静默返回 null。PyCore::next() 也会区分正常迭代结束和 iterator 异常。上述路径均先建立失败的 phpy PHPUnit 回归测试,其中对象调用行为另有 TypePHP PHPT 集成覆盖。
第三轮审计覆盖 PyCore Facade 的转换失败与函数缓存。PyCore::eval() 会在 globals 转换失败后立即抛出 PyError,PyCore::bytes() 对非字符串标量使用转换后的 zend_string,两者不再解引用空指针或错误的 zval union 字段而导致进程崩溃。PyCore::next() 同时释放参数转换产生的 iterator 引用。builtin/operator 函数缓存改用 std::string 内容键,不再把请求级 char* 地址作为长期 key,也避免同名动态调用不断重复缓存并增加 Python function 引用计数;调用存在但不可调用的 builtin 会释放临时引用并抛出 PyError(TypeError)。所有问题均由先失败的独立 PHPUnit 覆盖,其中两个崩溃用禁用 core dump 的隔离进程确认退出码 139 后再修复。
第四轮审计覆盖 Python 到 PHP 的同步回调边界。phpy 会将 Python keyword arguments 转换为 Zend named parameters,并在任一位置参数或命名参数转换失败后立即停止,不会执行只接收到部分参数的 PHP callable。PHPX 为 AOT 原生闭包生成并管理 Zend arg_info 参数名元数据,因此 Python kwargs 可以按名称绑定到 TypePHP 闭包,而不是依赖参数位置或降级为字符串 callable。phpy PHPUnit、PHPX 单元测试和 TypePHP PHPT 分别覆盖了转换失败、Zend 命名绑定以及完整的 Python→TypePHP 回调链路。
第五轮审计覆盖 Python 字符串跨 Zend 边界时的异常和所有权。Python 孤立代理字符无法编码为 UTF-8 时,phpy.String、动态 PHP 类名、字典键、PyObject::__toString() 和 Python 异常消息格式化都不会再使用空指针或未初始化长度;修复前相关隔离测试会退出 139 或尝试分配异常大的内存。StrObject 现在具有显式有效状态,所有调用方必须在访问指针前检查转换结果;异常消息的字符串化仅作为 best-effort 辅助信息,失败时保留原始 Python error/type/value,并清理临时 CPython error indicator。new_string() 同时补齐 Zend carrier 析构注册,定长字符串直接取得唯一的 zend_string 引用,消除了成功路径的泄漏和未初始化 zval。
第六轮引用审计修复了 PySequence::slice() 的 new-reference 泄漏。切片在包装为 Zend PyObject 后会释放 CPython API 返回的原始所有权,同时保留 wrapper 自己持有的引用;由 sys.getrefcount() 压力测试验证重复创建并销毁切片不会继续增加元素引用计数。切片创建失败也会在接触空指针前转换为 PyError。
内存门禁使用 Valgrind Memcheck 执行。测试关闭 Zend allocator 与 PCRE JIT,在最小独立进程中分别循环 100 次 PHP Closure kwargs 回调、可调用 PHP 对象 kwargs 回调、sequence slice 创建销毁、无效 Unicode 的对象字符串化、字典键转换和异常格式化。结果为 0 invalid-access、0 definite leak、0 indirect leak;进程退出时由 PHP/CPython 保留的 493,106 bytes 均为 still-reachable,不计为泄漏。ASan 扩展无法安全 dlopen 到当前启用了 RTLD_DEEPBIND 的非 ASan PHP,因此本轮采用不要求 PHP 同步重编译的 Valgrind 作为内存检查工具。
阶段门禁
- 当前阶段的失败测试未先建立,不开始实现。
- 当前阶段所有测试未通过,不进入下一阶段。
- phpy 的行为变更必须先在 phpy 仓库增加 PHPUnit/pytest 测试。
- 每个已修复 BUG 必须保留独立回归测试。
- 不以修改第三方测试期望来掩盖实现差异。