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_DEEPBINDsolibphp.so's own internal calls prefer its own (ZTS) symbols over the host's NTS ones.RTLD_GLOBALso subsequently-dlopen'd extensions can also seelibphp.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
libphpre-link treatment if it references core Zend symbols. - Shutdown is minimal. The loader calls
php_module_shutdown,sapi_shutdown, andtsrm_shutdown, but interleaving multiple startups or reusing the FFI handle is out of scope here.
References
- https://github.com/adsr/php-meta-sapi -- source of the trick.
- adsr/php-meta-sapi#1 -- the question this repo is the experiment for.
- https://www.php.net/manual/en/book.parallel.php -- ZTS-only extension used as the demo payload.
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 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 sj-i/ffi-zts 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
Embed laravel blade component + validation
Sequential way to run Symfony Processes in parallel
Extracts information about medias on the web, like youtube videos, twitter statuses or blog articles.
MediaWiki extension that allows embedding external content, specified by URL, into your wiki pages
This Bundle provides threaded comment functionality for Symfony applications
Generic PHP Threads library using only pure PHP
统计信息
- 总下载量: 49
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 37
- 依赖项目数: 1
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-04-20