2020-01-21 编译 PHP 扩展时,链接静态库

概述

PHP 的扩展库有两种编译形式。
一种是在编译 PHP 时,指定参数,将扩展静态化编译到 PHP 二进制文件中。
另外一种是在 PHP 编译完成后,利用 PEAR 机制编译扩展的动态库,供 PHP 启动时加载。
前一种当然更好,毕竟启动的时间是一次性,可以接受。
实际上很多时候,我们都是采用的后一种方式,因为比较灵活,安装以后还可以随时追加扩展库进去。

编译 PHP 动态扩展库的时候,又存在扩展库对其它库的依赖。比如常用的 zip 扩展,就依赖于 libzip 这个第三方库。
一般情况下,我们最常用的办法是用 yum 安装第三方依赖,然后再用 pecl cli,或者下载编译的方式,安装 php 扩展。
这样安装出来的扩展,十之八九都是 php ext 的动态库去调用第三方动态库。
通常情况下,倒是不会存在什么特别的问题,性能方面也几乎没有什么影响。
我个人的经验中,遇到过一次相关的事故,是某位哥们儿用 yum update -y 命令升级系统所有 package,包含 glibc-devel 这个包。
恰好呢,那段时间 glibc 因为安全问题,升级挺频繁的,并且某个常用的底层函数(我忘了具体名称,可能是 hostname 那次)没有向前兼容。
这下好了,yum 命令执行以后,好多程序相继崩溃,监控服务一直在重试这些程序,一直拉不起来。排查好久才找到问题。downgrade glibc-devel 后,险情接触。
这是我第一次遇到调用动态库的坑。

实际情况中,还有另外一种情况需要考虑,就是 deployment。
扩展库依赖第三方动态库,那么部署的时候,就要先部署第三方动态库。而所需的第三方动态库可能和现网服务器上同名第三方库版本冲突。
此时最直接的办法是换个目录安装新版本的第三方动态库。但后果可能像我之前举例中讲到的,某个程序因为第三方动态库版本之间的不兼容,莫名其妙出错,运行不起来。
之前举例的 zip 扩展就有这种情况。yum 安装的 libzip 有一些别的程序在用,但版本过低(0.10.1),不满足 php zip 扩展的编译要求(> 1.2)。
升级 libzip-devel package 吧,可能出现别的依赖程序不兼容;不升级吧,php zip 扩展又编译不了。

此时最简单的解决办法,就是在另外一台服务器上,把 php 的扩展编译好,里面做好第三方库的静态库。
部署的时候,把 php 扩展库的 .so 文件打包发布即可。只要 OS release 一致,这样直接发布 binary 就是没问题的。
即便未来这种方法不灵,也是很容易发现错误的。问题最晚在部署阶段就能被发现。

嗯,你们可能也看出来了,我是 fat-binary 策略的拥趸。

那安装 php 扩展时,怎么发布这种 fat binary 的动态库呢?下面举两个例子。

zip 扩展

mkdir -p /data/soft && cd $_  # 切换工作目录

# compile & install libzip 1.3.2
curl -Lo /data/soft/libzip-1.3.2.tar.xz https://libzip.org/download/libzip-1.3.2.tar.xz
tar -C /data/soft -Jxf /data/soft/libzip-1.3.2.tar.xz
cd /data/soft/libzip-1.3.2
./configure --prefix=/usr/local/libzip-1.3.2 --enable-static --disable-shared --with-pic
make -j$(nproc) && make install

# compile & install php zip ext
curl -Lo /data/soft/zip-1.15.5.tgz http://pecl.php.net/get/zip-1.15.5.tgz
tar -C /data/soft -zxf /data/soft/zip-1.15.5.tgz
cd /data/soft/zip-1.15.5
phpize
env CFLAGS="-I/usr/local/libzip-1.3.2/include" \
  LDFLAGS="-L/usr/local/libzip-1.3.2/lib -lzip -lbz2 -lz" \
  ./configure --with-libzip=/usr/local/libzip-1.3.2 --enable-zip
make -j$(nproc) && make install

PS:备注。

第一,使用 libzip-1.3.2 而不是最新的 libzip-1.5.2,是因为 libzip 从 1.4.0 开始使用 cmake 作为编译工具,而且限定 cmake > 3.0.2。可是 CentOS 7.6 下用 yum 也只能升级到 cmake 2.8.6。往下,libzip-1.3.2 还是用 gmake 来安装的,也够用,就它了。
第二,安装 libzip 时指定了不安装 shared lib,只安装 static lib,开启了 --with-pic 指令,针对平台做编译优化。这个版本的 libzip 安装到一个特定目录去。
第三,编译 php zip 扩展时,用 PKG_CONFIG_PATH 环境变量不好使,反复尝试,最后用 pkg-config 命令获取了查询结果,用 env 的方式把结果添加到 configure 命令中去,就行了。是临时取巧的做法。

lua 扩展

mkdir -p /data/soft && cd $_  # 切换工作目录

# compile & install liblua
curl -Lo /data/soft/lua-5.3.5.tar.gz https://www.lua.org/ftp/lua-5.3.5.tar.gz
tar -C /data/soft -zxf /data/soft/lua-5.3.5.tar.gz
cd /data/soft/lua-5.3.5
make CFLAGS=-fPIC PLAT=linux -j$(nproc)
make install INSTALL_TOP=/usr/local/lua-5.3.5

# compile & install lua ext
curl -Lo /data/soft/lua-2.0.6.tgz http://pecl.php.net/get/lua-2.0.6.tgz
tar -C /data/soft -zxf /data/soft/lua-2.0.6.tgz
cd /data/soft/lua-2.0.6
phpize
./configure --with-lua=/usr/local/lua-5.3.5
make -j$(nproc) && make install

PS:备注一下。

第一,编译 liblua 时,带上 CFLAGS=-fPIC 参数,是因为 liblua.a 需要链接到 php 扩展中去,如果不带 PIC 指令,liblua 和 lua 扩展两者编译出来的符号表会有冲突。
第二,把 liblua 安装到一个特定的目录,而不是安装到默认目录(默认目录是 /usr/local),和 yum 安装的 liblua 严格分离。
第三,编译 lua 扩展时,用 --with-lua 参数指定要链接的 liblua 位置即可。上面安装 liblua 的方法,只会安装 liblua 的静态库,所以只要指定这个 liblua 位置,链接的时候就一定会链接上这个静态库。

结果

从截屏中可以看出,两个扩展都解除了对第三方库的依赖。zip ext 不依赖 libzip 动态库,lua ext 也不依赖 liblua 了。达到了预期的目的。

安装完成后的截屏

这样发布的 php 扩展库,不用担心未来系统升级时收到影响。对于运维权限不在自己手上的情况,甚至与人共用一套系统的情况,真是必备良药。

推荐阅读更多精彩内容