PHP做天文计算的开发者有个共同噩梦:要么用命令行工具 spawning 子进程慢如蜗牛,要么用纯 PHP 近似算法误差大到能算出两个月亮。Swiss Ephemeris 是天文计算领域的金标准,NASA 级别的精度,但 PHP 生态里一直没有靠谱的调用方式。
开发者 Jayesh Mepani 在 2024 年做了个统计:Packagist 上 12 个相关包,8 个已经三年没更新,4 个需要手动编译 C 代码。他花了 47 天写了一个 FFI 包装器,把 Swiss Ephemeris 的 C 库直接桥接到 PHP 8.3+。没有中间层,没有文本解析,就是原生函数调用。
为什么之前的方案都"瘸腿"
PHP 社区处理 Swiss Ephemeris historically 有三条路。第一条是 exec() 调命令行,每次计算 spawn 新进程,延迟 50-200ms 起步,高并发直接崩盘。第二条是解析文本输出,用正则从字符串里抠数字,格式一变就崩溃。第三条是纯 PHP 重写算法,行星位置误差能到 0.5 度——占星软件里这就是"把金牛座算成双子座"的灾难。
Jayesh 在 README 里列了个对照表:CLI wrapper 方案安装最简单但性能最差,纯 PHP 方案零依赖但精度最差,源码编译方案精度最高但"需要用户会 make && make install"。他的目标是把第三列的精度、第一列的易用性、第二列的零依赖拼在一起。
FFI(Foreign Function Interface,外部函数接口)在 PHP 7.4 就引入了,但天文计算领域几乎没人用。原因很简单:Swiss Ephemeris 的 API 表面太大,200+ 个函数、600+ 个宏常量、几十种结构体,手工绑定等于写一本字典。
1:1 映射是什么意思
Jayesh 的包装器有个硬指标:zero tolerance for omissions。原库的 swephexp.h 头文件里定义了什么,PHP 侧就暴露什么。行星 ID 从 SE_SUN 到 SE_PLUTO 再到小行星编号,全部保留。宫位系统从 Placidus 到 Whole Sign 到 Koch,一个不落。恒星黄道模式、日月食计算、星历表文件操作,全部直通。
代码长这样:
$sweph = new SwissEphFFI(); $jd = $sweph->swe_julday(2000, 1, 1, 12.0, SwissEphFFI::SE_GREG_CAL); $xx = $sweph->getFFI()->new("double[6]"); $serr = $sweph->getFFI()->new("char[256]"); $sweph->swe_calc_ut($jd, SwissEphFFI::SE_SUN, SwissEphFFI::SEFLG_SPEED, $xx, $serr);
swe_julday 是儒略日转换,swe_calc_ut 是计算行星位置,SEFLG_SPEED 表示同时返回速度。这些函数名、参数顺序、错误处理方式,和 C 库完全一致。懂 Swiss Ephemeris 的开发者零学习成本,不懂的可以直接查原库文档。
预编译二进制是另一个关键决策。Windows 用 DLL,Linux 用 .so,macOS 用 .dylib,打包在 release 里。用户 composer require 之后,按平台配好扩展路径就能跑。Jayesh 测试了 Ubuntu 22.04、Debian 12、macOS 14、Windows 11,FFI 加载成功率 100%。
谁真的需要这个
占星软件是 obvious 的场景。出生星盘计算需要行星精确位置、宫位划分、相位角度,误差超过 0.01 度就会被专业用户骂。天文教育平台需要可视化行星运动,实时渲染不能等 200ms 的进程 spawning。历法转换工具、农业节气计算、甚至某些宗教节日的日期推算,都依赖星历表。
一个 Laravel 开发者反馈:之前用 Python 微服务做天文计算,PHP 调 HTTP API,架构复杂得像"用卫星电话叫外卖"。现在直接用 FFI,控制器里 5 行代码出结果,响应时间从 300ms 降到 3ms。
框架无关设计是刻意为之。Jayesh 自己不用 Laravel,但写了 service provider 示例。核心类 SwissEphFFI 零依赖,可以在 Slim、Symfony、纯 PHP 脚本里直接用。这种"我不替你选框架"的态度,在 PHP 生态里反而稀缺。
FFI 的隐性成本
PHP FFI 不是免费午餐。每次跨语言调用有固定开销,大概 0.1-0.5 微秒。天文计算本身是重 CPU 操作,单次 swe_calc_ut 可能涉及迭代求解、星历表插值、相对论修正,耗时 10-100 微秒。FFI 开销占比 1% 以下,可以忽略。
内存管理需要小心。C 库返回的 double[6] 数组,PHP 侧用 FFI::new 分配,需要手动管理生命周期。Jayesh 的示例代码里用了 $xx = $sweph->getFFI()->new("double[6]"),这是 FFI 的标准模式,比自动垃圾回收可控,但也意味着开发者要理解 C 的内存语义。
星历表文件是另一个坑。Swiss Ephemeris 需要二进制星历表(DE440、DE441 等),单个文件 100MB 起步。Jayesh 没有打包这些文件,文档里写明"请自行下载并配置路径"。这是合法选择——星历表有 NASA 的许可条款,且更新频繁,硬编码版本反而麻烦。
社区反应与后续
GitHub 仓库在 2024 年 11 月开源,两周内 87 个 star,3 个 issue 全是"怎么在 Windows 上配 FFI"。Jayesh 的回复很直接:"先确认 php.ini 里 ffi.enable=true,然后检查 DLL 路径有没有中文。" 没有"感谢您的反馈我们会考虑"的客套。
Packagist 下载量增长曲线很有意思:前 30 天平均每天 5 次,一个占星软件博主发了视频教程后,单日跳到 200+。这说明小众工具的市场,往往藏在垂直社区的口口相传里。
版本规划里有个细节:Jayesh 承诺"跟随上游 Swiss Ephemeris 更新,但不做 API 抽象"。如果原库加了新函数,包装器会同步加,但不会包成 "calculatePlanetPosition($planet, $date)" 这种高层接口。他的理由是:"抽象是 opinionated 的,1:1 是 unopinionated 的。我宁愿让用户多写两行代码,也不替他们决定怎么封装。"
PHP 8.3+ 的 requirement 也引发过讨论。8.3 在 2023 年 11 月发布,到 2024 年底市场占有率约 15%。Jayesh 的回应是:"FFI 在 8.3 有性能优化,且我需要 typed class constants 来映射宏。用旧版本的用户可以 fork 回去自己改。" 这种"我不伺候所有人"的立场,在小众工具里反而建立信任。
一个占星软件作者在 issue 里写:"终于不用在 PHP 和 Python 之间来回传数据了,我的 Docker 镜像小了 400MB。" 另一个天文教育平台的开发者说:"之前用纯 PHP 近似算法,用户投诉火星位置差了 0.3 度,现在精确到 0.001 度以内。"
热门跟贴