配置和鏈接
所有前面示例中的代碼, 都是你曾經在php用戶空間編寫過代碼的C語言的獨立版本. 如果你做的項目需要和php擴展進行粘合, 那麼你就至少需要連結一個外部函式庫.
autoconf
在一個簡單的應用中, 你可能已經在你的Makefile中增加了下面這樣的CFGS和LDFLAGS.
想要建立你的應用卻沒有libfoobar的人, 或將libfoobar安裝到其他位置的人, 將會得到一個處理過的錯誤訊息, 用於幫助他找到錯誤原因.在過去十年開發的多數開發原始碼軟體(OSS)以及PHP都利用了一個實用工具autoconf, 透過一些簡單的宏來產生複雜的configure腳本. 這個產生的腳本會執行查找依賴庫已經頭文件是否安裝的工作. 基於這些資訊, 一個套件可以自訂建置程式碼行, 或在編譯的時間被浪費之前提供一個有意義的錯誤訊息.
在建構php擴充時, 無論你是否計劃公開發布, 都需要利用這個autoconf機轉. 即使你對autoconf已經很熟悉了, 也請花幾分鐘時間閱讀本章, php中引入了一些一般安裝的autoconf沒有的自定義宏.
和傳統的autoconf步驟(集中的configure.in文件包含了包的所有配置宏)不同, php只是用configure.in管理許多位域源碼樹下小的config.m4腳本的協調, 包括各個擴展, SAPI, 核心自身, 以及ZendEngine.
你已經在前面的章節看到了一個簡單版本的config.m4. 接下來, 我們將在這個文件中增加其他的autoconf語法, 讓你的擴展可以收集到更多的配置時信息.
庫的查找
config.m4腳本最多是用於檢查依賴庫是否已安裝. 擴展例如mysql, ldap, gmp以及其他設計為php用戶空間和c庫實現的其他功能之間的粘合層的擴展. 如果它們的依賴庫沒有安裝, 或者安裝的版本太舊, 要么會編譯錯誤, 要么會導致產生的二進制無法運行.
頭文件掃描
頭文件掃描
對依賴庫掃描中最對依賴庫掃描中最對依賴庫掃描中最最依賴庫掃描簡單的一步就是檢查你的腳本中的包含文件, 它們將在鏈接時使用. 下面的代碼嘗試在一些常見位置查找zlib.h:
CFLAGS = ${CFLAGS} -I/usr/local/foobar/include LDFLAGS = ${LDFLAGS} -lfoobar -L/usr/local/foobar/lib
config.m4文件很明顯比你迄今為止使用的要大. 幸運的是, 它的語法非常的簡單易懂並且如果你熟悉bash腳本, 對它也就不會陌生.
🎜🎜文件和第5章"你的第一個擴展"中第一次出現的一樣, 都是以PHP_ARG_WITH()宏開始. 這個宏的行為和你用過的PHP_ARG_ENABLE()宏一樣, 不過它將導致./configure時的選項是--with-extname/--without- extname而不再是--enable-extname/--disable-extname.🎜🎜回顾这些宏, 它们的功能是等同的, 不同仅在于是否让终端用户给你的包一些暗示. 你可以在自己创建的私有扩展上使用任意一种方式. 不过, 如果你计划公开发布, 那就应该知道php正式的编码标准, 它指出enable/disable用于哪些不需要链接外部库的扩展, with/without则反之.
由于我们这里假设的扩展将链接zlib库, 因此你的config.m4脚本要以查找扩展源代码中将包含的zlib.h头文件. 这通过检查一些标准位置/usr, /usr/local, /opt中include/zlib目录下的zlib.h完成对其下两个目录的定位.
如果找到了zlib.h, 则将基路径设置到临时变量ZLIB_DIR中. 一旦循环完成, config.m4脚本检查ZLIB_DIR是否包含内容来确定是否找到了zlib.h. 如果没有找到, 则产生一个有意义的错误让用户知道./configure不能继续.
此刻, 脚本假设头文件存在, 对应的库也必须存在, 因此在下面的两行使用它修改构建环境, 最终增加-lz -L$ZLIB_DIR/lib到LDFLAGS以及-I$ZLIB_DIR/include到CFLAGS.
最终, 输出一个确认消息指示zlib安装已经找到, 并且在编译期间使用它的路径. config.m4的其他部分从前面部分的学习中你应该已经熟悉了. 为config.h定义一个#define, 定义扩展并指定它的源代码文件, 同时标识一个变量完成将扩展附加到构建系统的工作.
功能测试
迄今为止, 这个config.m4示例指示查找了需要的头文件. 尽管这已经够用了, 但它仍然不能确保产生的二进制正确的进行链接, 因为可能不存在匹配的库文件, 或者版本不正确.
最简单的检查zlib.h对应的libz.so库文件是否存在的方式就是检查文件是否存在:
if ! test -f $ZLIB_DIR/lib/libz.so; then AC_MSG_ERROR([zlib.h found, but libz.so not present!]) fi
当然, 这仅仅是问题的一面. 如果安装了其他的同名库但和你要查找的库不兼容怎么办呢? 确保你的扩展可以成功编译的最好方式是测试找到的库实际编译所需的内容. 要这样做就需要在config.m4中PHP_ADD_LIBRARY_WITH_PATH调用之前加入下面代码:
PHP_CHECK_LIBRARY(z, deflateInit,,[ AC_MSG_ERROR([Invalid zlib extension, gzInit() not found]) ],-L$ZLIB_DIR/lib)
这个工具宏将展开输出一个完整的程序, ./configure将尝试编译它. 如果编译成功, 表示第二个参数定义的符号在第一个参数指定的库中存在. 成功后, 第三个参数中指定的autoconf脚本将会执行; 失败后, 第四个参数中指定的autoconf脚本将执行. 在这个例子中, 第三个参数为空, 因为没有消息就是最好的消息(译注: 应该是unix哲学之一), 第五个参数也就是左后一个参数, 用于指定额外的编译器和链接器标记, 这里, 使用-L致命了一个额外的用于查找库的路径.
可选功能
那么现在你已经有正确的库和头文件了, 但依赖的是所安装库的哪个版本呢? 你可能需要某些功能或排斥某些功能. 由于这种类型的变更通常涉及到某些特定入口点的增加或删除, 因此可以重用PHP_CHECK_LIBRARY()宏来检查库的某些能力.
PHP_CHECK_LIBRARY(z, gzgets,[ AC_DEFINE(HAVE_ZLIB_GETS,1,[Having gzgets indicates zlib >= 1.0.9]) ],[ AC_MSG_WARN([zlib < 1.0.9 installed, gzgets() will not be available]) ],-L$ZLIB_DIR/lib)
测试实际行为
可能知道某个符号存在也还不能确保你的代码正确编译; 某些库的特定版本可能存在bug需要运行一些测试代码进行检查.
AC_TRY_RUN()宏可以编译一个小的源代码文件为可执行程序并执行. 依赖于传回给./configure的返回代码, 你的脚本可以设置可选的#define语句或直接输出消息(比如如果bug导致不能工作则提示升级)安全退出. 考虑下面的代码(摘自ext/standard/config.m4):
AC_TRY_RUN([ #include <math.h> double somefn(double n) { return floor(n*pow(10,2) + 0.5); } int main() { return somefn(0.045)/10.0 != 0.5; } ],[ PHP_ROUND_FUZZ=0.5 AC_MSG_RESULT(yes) ],[ PHP_ROUND_FUZZ=0.50000000001 AC_MSG_RESULT(no) ],[ PHP_ROUND_FUZZ=0.50000000001 AC_MSG_RESULT(cross compile) ]) AC_DEFINE_UNQUOTED(PHP_ROUND_FUZZ, $PHP_ROUND_FUZZ, [Is double precision imprecise?])
你可以看到, AC_TRY_RUN()的第一个参数是一块C语言代码, 它将被编译执行. 如果这段代码的退出代码是0, 位于第二个参数的autoconf脚本将被执行, 这种情况标识round()函数和期望一样工作, 返回0.5.
如果代码块返回非0值, 位域第三个参数的autoconf脚本将被执行. 第四个参数(最后一个)在php交叉编译时使用. 这种情况下, 尝试运行示例代码是没有意义的, 因为目标平台不同于扩展编译时使用的平台.
强制模块依赖
在php 5.1中, 扩展之间的内部依赖是可以强制性的. 由于扩展可以静态构建到php中, 也可以构建为共享对象动态加载, 因此强制依赖需要在两个地方实现.
配置时模块依赖
第一个位置是你在本章课程中刚刚看到的config.m4文件中. 你可以使用PHP_ADD_EXTENSION_DEP(extname, depname[ , optional])宏标识extname这个扩展依赖于depname这个扩展. 当extname以静态方式构建到php中时, ./configure脚本将使用这一行代码确认depname必须首先初始化. optional参数是一个标记, 用来标识depname如果也是静态构建的, 应该在extname之前加载, 不过它并不是必须的依赖.
这个宏的一个使用示例是pdo驱动, 比如pdo_mysql是可预知依赖于pdo扩展的:
ifdef([PHP_ADD_EXTENDION_DEP], [ PHP_ADD_EXTENSION_DEP(pdo_mysql, pdo) ])
要注意PHP_ADD_EXTENSION_DEP()宏被包裹到一个ifdef()结构中. 这是因为pdo和它的驱动在编译大于或等于5.0版本的php时都是存在的, 然而PHP_ADD_EXTENSION_DEP()宏是直到5.1.0版本才出现的.
运行时模块依赖
另外一个你需要注册依赖的地方是zend_module_entry结构体中. 考虑下面第5章中你定义的zend_module_entry结构体:
zend_module_entry sample_module_entry = { #if ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_SAMPLE_EXTNAME, php_sample_functions, NULL, /* MINIT */ NULL, /* MSHUTDOWN */ NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */ #if ZEND_MODULE_API_NO >= 20010901 PHP_SAMPLE_EXTVER, #endif STANDARD_MODULE_PROPERTIES };
增加运行时模块依赖信息就需要对STANDARD_MOUDLE_HEADER部分进行一些小修改:
zend_module_entry sample_module_entry = { #if ZEND_MODULE_API_NO >= 220050617 STANDARD_MODULE_HEADER_EX, NULL, php_sample_deps, #elif ZEND_MODULE_API_NO >= 20010901 STANDARD_MODULE_HEADER, #endif PHP_SAMPLE_EXTNAME, php_sample_functions, NULL, /* MINIT */ NULL, /* MSHUTDOWN */ NULL, /* RINIT */ NULL, /* RSHUTDOWN */ NULL, /* MINFO */ #if ZEND_MODULE_API_NO >= 20010901 PHP_SAMPLE_EXTVER, #endif STANDARD_MODULE_PROPERTIES };
现在, 如果ZEND_MODULE_API_NO高于php 5.1.0 beta发布版, 则STANDARD_MODULE_HEADER(译注: 这里原著笔误为STANDARD_MODULE_PROPERTIES)将被替换为略微复杂的结构, 它将包含一个指向模块依赖信息的引用.
这个目标结构体可以在你的zend_module_entry结构体上面定义如下:
#if ZEND_MODULE_API_NO >= 220050617 static zend_module_dep php_sample_deps[] = { ZEND_MODULE_REQUIRED("zlib") {NULL,NULL,NULL} }; #endif
和zend_function_entry向量类似, 这个列表可以有多项依赖, 按照顺序进行检查. 如果尝试加载某个依赖模块未满足, Zend将会中断加载, 报告不满足依赖的名字, 这样, 终端用户就可以通过首先加载其他模块来解决问题.
Windows方言
由于译者对windows环境不熟悉, 因此略过本节.
小结
如果你的扩展将在未知或不可控制的环境构建, 让它足够聪明以应付奇怪的环境就非常重要. 使用php提供的unix和windows上强有力的脚本能力, 你应该可以检测到麻烦并在未知的管理员需要电话求助之前给于她一个解决方案.
现在你已经有使用php api从头建立php扩展的基础能力了, 你可以准备学习一下使用php提供的扩展开发工具把自己从繁重的重复劳动中解放出来了, 使用它们可以快速, 准确的建立新扩展的原型.
以上就是[翻译][php扩展开发和嵌入式]第17章-php源代码的配置和链接的内容,更多相关内容请关注PHP中文网(www.php.cn)!