sj-i/ffi-zts 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

sj-i/ffi-zts

Composer 安装命令:

composer require sj-i/ffi-zts

包简介

Embed a ZTS PHP interpreter inside an NTS PHP process via FFI and run ZTS-only extensions (e.g. pecl/parallel) from a plain NTS CLI.

README 文档

README

Add-on ZTS VM for an NTS PHP: a plain non-thread-safe PHP CLI loads libphp.so from a thread-safe PHP build through FFI and executes scripts inside that embedded ZTS interpreter -- including scripts that use ZTS-only extensions such as pecl/parallel.

Originally prompted by adsr/php-meta-sapi#1, which asked whether adsr/php-meta-sapi's "PHP embedded in PHP" trick can be pushed one step further: use the embedded PHP to pick up ZTS extensions while the outer PHP stays NTS.

The short answer is yes, with two small pieces of glue.

Install

composer require sj-i/ffi-zts

sj-i/ffi-zts ships as a Composer plugin: on install it downloads the pre-built libphp.so matching your host's PHP minor / CPU arch / libc into vendor/sj-i/ffi-zts/bin/libphp.so.

Major tracks the host PHP minor: 1.x = PHP 8.4, 2.x = PHP 8.5. Composer resolves the right major automatically from your host's php -v; the 2.x line picks up upstream's static opcache, which makes opcache.preload under the embed work out of the box (see docs/PERFORMANCE.md for measured numbers).

Trusting the plugin

Composer 2.2+ asks you to trust a new plugin before running it. In interactive shells you get a (y/N) prompt on first install. In CI / non-interactive environments, whitelist it up front:

composer config allow-plugins.sj-i/ffi-zts true
composer require sj-i/ffi-zts

If you also install the sj-i/ffi-zts-parallel satellite, allow it too:

composer config allow-plugins.sj-i/ffi-zts true
composer config allow-plugins.sj-i/ffi-zts-parallel true
composer require sj-i/ffi-zts-parallel

Manual install / retry

If the plugin was skipped (--no-plugins, network outage, binary not yet published for a new PHP minor, ...), retry the binary fetch on demand:

vendor/bin/ffi-zts install

vendor/bin/ffi-zts info reports the resolved host profile and tells you whether the binary is in place.

Usage

<?php
require __DIR__ . '/vendor/autoload.php';

use SjI\FfiZts\FfiZts;

FfiZts::boot()
    ->runScript(__DIR__ . '/worker.php');

FfiZts::boot() auto-resolves vendor/sj-i/ffi-zts/bin/libphp.so; $worker.php runs inside the embedded ZTS interpreter.

For real OS-thread parallelism via parallel\Runtime, install the satellite package sj-i/ffi-zts-parallel instead -- it pulls in this core package transitively and wires parallel.so through the embed.

See docs/DESIGN.md for the full architecture.

Experimental loader (ffi-zts.php)

Before the library was packaged for Composer, the same mechanism lived in a single ffi-zts.php script. It still works and is the quickest way to inspect the ZTS embed end-to-end without composer involvement:

# Host PHP is the system NTS build (e.g. /usr/bin/php).
php ffi-zts.php \
    --libphp-path=/home/user/php-zts/lib/libphp.so \
    --ini=examples/ffi-zts.ini \
    examples/test-parallel.php

Expected output:

== host info ==
PHP_VERSION = 8.4.19
PHP_SAPI    = ffi-zts
PHP_ZTS     = 1
parallel?   = yes
...
worker id=0 pid=... sum=1999999000000 zts=1 took=10ms
worker id=1 pid=... sum=1999999000000 zts=1 took=10ms
worker id=2 pid=... sum=1999999000000 zts=1 took=10ms
worker id=3 pid=... sum=1999999000000 zts=1 took=10ms

Verify with php -v that the outer process is indeed NTS; the embedded SAPI reports PHP_ZTS=1 and PHP_SAPI=ffi-zts.

Building the binaries from source

# 1. ZTS PHP (embed SAPI) into /home/user/php-zts
./scripts/build-zts-php.sh

# 2. Two parallel.so builds: vanilla + FFI-linked
./scripts/build-parallel.sh

Host-side requirements: a working NTS PHP 8.x with ffi enabled (/etc/php/8.4/cli/conf.d/20-ffi.ini on Debian/Ubuntu).

Why two parallel.sos?

Because symbol resolution is the whole game here.

When libphp.so (ZTS) is dlopen'd into an NTS PHP process, the host binary has already exported its own NTS copies of the Zend runtime (zend_register_functions, zend_register_internal_class_with_flags, compiler_globals, ...). The runtime linker puts the main executable first in the global search order, so any later library that references those symbols resolves to the NTS copy by default.

ffi-zts.php works around this with two flags on its own dlopen() of libphp.so:

  • RTLD_DEEPBIND so libphp.so's own internal calls prefer its own (ZTS) symbols over the host's NTS ones.
  • RTLD_GLOBAL so subsequently-dlopen'd extensions can also see libphp.so's ZTS symbols.

That is enough to run the embedded ZTS interpreter cleanly. But when the embedded PHP honors extension=parallel.so, its internal php_load_shlib() calls dlopen(parallel.so, RTLD_GLOBAL|RTLD_DEEPBIND) too -- and RTLD_DEEPBIND only promotes symbols from the library and its DT_NEEDED dependencies. Stock parallel.so has no DT_NEEDED libphp.so, so its undefined references fall through to the global scope, which still has the host NTS symbols at the front. Result: parallel's MINIT calls the NTS zend_register_internal_class_with_flags on the NTS class table and segfaults.

The fix is to re-link parallel.so against libphp.so:

LDFLAGS="-Wl,--no-as-needed -L$PREFIX/lib -Wl,-rpath,$PREFIX/lib -lphp" \
    ./configure --with-php-config=$PREFIX/bin/php-config

Now parallel.so has libphp.so in DT_NEEDED, so with RTLD_DEEPBIND its references to the Zend runtime resolve through libphp.so (ZTS) before reaching the host (NTS).

This re-linked parallel.so is not usable from the native ZTS CLI (double-loading libphp.so crashes the self-contained CLI binary), which is why the build script keeps it in a separate extensions/ffi-zts/ directory.

Why not dlmopen?

dlmopen(LM_ID_NEWLM, ...) would give perfect isolation, and that was the first thing tried -- the baseline "hello" script worked cleanly. But as soon as the embedded ZTS tried to dlopen(parallel.so) from inside its new namespace, glibc segfaulted in ld-linux.so's add_to_global_resize (./elf/dl-open.c:126). This is a long-standing limitation: loading any library that pulls in libpthread-ish behavior into a non-default linker namespace is fragile in glibc. So dlmopen is the nicer theory, and RTLD_DEEPBIND + libphp-linked extension is the one that actually runs.

Caveats

  • The host NTS process is still fundamentally single-threaded PHP. Only code running inside the embedded ZTS interpreter (and in threads spawned from it by parallel\Runtime) benefits from thread-safety.
  • Each ZTS extension you want to use from the embedded side may need the same libphp re-link treatment if it references core Zend symbols.
  • Shutdown is minimal. The loader calls php_module_shutdown, sapi_shutdown, and tsrm_shutdown, but interleaving multiple startups or reusing the FFI handle is out of scope here.

References

sj-i/ffi-zts 适用场景与选型建议

sj-i/ffi-zts 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 49 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 04 月 20 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「threads」 「parallel」 「embed」 「composer-plugin」 「ffi」 「zts」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 sj-i/ffi-zts 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 sj-i/ffi-zts 我们能提供哪些服务?
定制开发 / 二次开发

基于 sj-i/ffi-zts 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 49
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 37
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-20