这个专栏的主要目的是带领大家理解PHP的底层机制,并掌握PHP扩展开发的基本要领。你会发现,在漫山遍野的PHP相关书籍、教程中,这样的内容寥寥无几。业界元老Sara Golemon女士曾写过一本书,_Extending and Embedding PHP_,于2006年出版。Zend API在每一个版本都有变化,以至于如今这本书已经没有太大参考价值了。目前,基本上只有PHP Internals Book和PHP Internals两个非官方站点提供稍微全面些的PHP7扩展开发的指导。然而,它们只起到了官方没有提供的API文档的作用(是的,除了几行少得可怜的注释,官方并没有提供任何API文档),在实际开发中,很多都需要自己摸索,在不断地调试、阅读PHP源码后,问题才能迎刃而解。所以我写这个专栏,除了讲一些基础之外,我还会把我在做PHP扩展开发的过程中踩过的坑分享给大家,让大家少走弯路,能更快上手。毕竟优雅高效开发才是PHPer们所追求的目标。
在PHP7的年代,userland PHP的性能是足够的。很多时候我们遇到的性能瓶颈都是出在I/O或者业务逻辑上,而不是PHP本身的执行速度不够。而像计算密集的程序,比如一些算法,我们不会拿PHP去做。
那什么时候我们需要写PHP扩展呢?
Generator
,所以如果我们想要在PHP中使用协程,就必须在底层实现一个上下文切换的库。最重要的一点,掌握PHP扩展开发的技术,可以给PHP带来无限的可能,而不是局限于Web开发的小领域中。
阅读本专栏文章需要掌握以下基础。
之后我写文章时默认大家有这样的基础。不然事无巨细,连malloc
是什么都要展开讲,对于那些有基础的读者来说可谓是一种折磨。
PHP源码仓库的ext目录下有一个shell脚本ext_skel(从PHP7.3起换成了一个PHP脚本ext_skel.php)。这个脚本可以用来生成一个最小结构的可用的PHP扩展,方便开发者在其基础上进行开发。
我们先用它生成一个名为foo的扩展。
./ext_skel --extname=foo
ls -R foo
可以看到生成的目录下有以下文件:
foo/:
config.m4 config.w32 CREDITS EXPERIMENTAL foo.c foo.php php_foo.h testsfoo/tests:
001.phpt
我们现在可以看到生成的config.m4脚本。这个脚本在PHP扩展中至关重要,它告诉PHP构建系统应该如何构建这个扩展。我们可以调用acinclude.m4中定义的M4宏来方便我们编写配置脚本。acinclude.m4有详细的注释,所以不难理解。大家也可以阅读官方扩展以及PECL扩展的config.m4脚本来熟悉一下写法。下面我会讲解几个最常用的宏的使用方法。
我们知道,当我们执行phpize
后,PHP编译系统将使用autoconf,根据config.m4生成configure脚本。我们往往希望在执行configure脚本时指定某些特定参数,比如--enable-pcntl
, --with-curl=/usr/local
。我们可以在config.m4中调用PHP_ARG_ENABLE
和PHP_ARG_WITH
这两个宏来实现。
dnl
dnl PHP_ARG_ENABLE(arg-name, check message, help text[, default-val[, extension-or-not]])
dnl Sets PHP_ARG_NAME either to the user value or to the default value.
dnl default-val defaults to no. This will also set the variable ext_shared,
dnl and will overwrite any previous variable of that name.
dnl If extension-or-not is yes (default), then do the ENABLE_ALL check and run
dnl the PHP_ARG_ANALYZE_EX.
dnl
其中,如果一个参数的extension-or-not
为yes
,则该参数表示这个扩展本身是否会被编译。一般来说,一个扩展有且仅有一个这样的参数,且它的arg-name
为这个扩展的名称。
check message
为configure脚本执行时会输出的信息:
checking for foo support... yes
help text
为执行./configure --help
时对应的提示信息:
Optional Features and Packages:--enable-foo-debug Compile with debugging symbols
default-val
为当你不指定这个参数的情况下生成的对应变量的值。如果指定了,--enable-foo
会置$PHP_FOO
为yes
,而--disable-foo
会置no
。如果是--enable-foo=bar
,那它就和with等价,置$PHP_FOO
为等号后的字符串。注意,不论arg-name
为如何,这个对应的变量永远是大写,而且"-"会被转换为"_"。
使用PHP_NEW_EXTENSION
将扩展添加到构建中。
dnl
dnl PHP_NEW_EXTENSION(extname, sources [, shared [, sapi_class [, extra-cflags [, cxx [, zend_ext]]]]])
dnl
dnl Includes an extension in the build.
dnl
dnl "extname" is the name of the extension.
dnl "sources" is a list of files relative to the subdir which are used
dnl to build the extension.
dnl "shared" can be set to "shared" or "yes" to build the extension as
dnl a dynamically loadable library. Optional parameter "sapi_class" can
dnl be set to "cli" to mark extension build only with CLI or CGI sapi's.
dnl "extra-cflags" are passed to the compiler, with
dnl @ext_srcdir@ and @ext_builddir@ being substituted.
dnl "cxx" can be used to indicate that a C++ shared module is desired.
dnl "zend_ext" indicates a zend extension.
extname
为扩展的名称。
sources
为源文件的列表,多个文件之间用空格分隔。
shared
为该扩展是否要编译为shared object,从而可以在php.ini中通过指定extension=foo
选择性加载。这个参数应该设定为$ext_shared
,由configure脚本进行判断。
sapi-class
如果设为cli
,则限制该扩展只能应用于PHP-CLI和PHP-CGI。否则,该扩展可以在任何sapi上使用。
extra-cflags
等同于CFLAGS+=" ..."
或者CXXFLAGS+=" ..."
,比如我们的扩展使用了C++11的语法,就可以在这里指定-std=c++11
。
cxx
为yes
表明在link时libtool会选择g++而不是cc,如果你的扩展是用C++编写的,建议设置为yes
,否则你需要-lstdc++
(除非你的依赖包含了其他C++库,已经连接了libstdc++)。
zend-ext
若为yes
,则表明这是一个Zend扩展而不是PHP扩展。在一般的应用场合,PHP扩展就足以满足我们的要求。而且编写Zend扩展要求开发者对Zend引擎有着深刻的理解。我们短时间内不做讨论。
我们的PHP扩展往往需要依赖其他的库。
宏PHP_ADD_INCLUDE
可以用来添加额外的包含头文件的目录。额外的目录将会被添加到参数$INCLUDES
中。
dnl
dnl PHP_ADD_INCLUDE(path [,before])
dnl
dnl add an include path.
dnl if before is 1, add in the beginning of INCLUDES.
dnl
宏PHP_CHECK_LIBRARY
可以用来判断一个库是否有效。
dnl
dnl PHP_CHECK_LIBRARY(library, function [, action-found [, action-not-found [, extra-libs]]])
dnl
dnl Wrapper for AC_CHECK_LIB
dnl
function
为用来测试的函数。configure脚本会通过该函数的符号是否存在来判断这个库是否有效。
action-found
为测试成功后将要执行的脚本。action-not-found
为测试失败后执行的脚本。
extra-libs
为测试时额外的$LDFLAGS
。
以下例子来自cURL扩展。
PHP_CHECK_LIBRARY(curl, curl_easy_perform,
[
AC_DEFINE(HAVE_CURL, 1, [ ])
], [
AC_MSG_ERROR(There is something wrong. Please check config.log for more information.)
], [
$CURL_LIBS
])
宏PHP_ADD_LIBRARY
可以用来添加要连接的库。
dnl
dnl PHP_ADD_LIBRARY(library[, append[, shared-libadd]])
dnl
dnl add a library to the link line
dnl
library
为库的名称。
如果append
为1,则该库会被添加到shared-libadd
变量的尾部。否则添加到变量的首部。
注意,shared-libadd
变量必须为大写库名加“_SHARED_LIBADD”。比如FOO_SHARED_LIBADD
。
Bash命令pkg-config
可以为我们提供库的信息。
参数--cflags
为使用该库所需要额外指定的$CFLAGS
,主要是头文件包含目录。常配合宏PHP_EVAL_INCLINE
使用。
dnl
dnl PHP_EVAL_INCLINE(headerline)
dnl
dnl Use this macro, if you need to add header search paths to the PHP
dnl build system which are only given in compiler notation.
dnl
参数--libs
为额外的$LDFLAGS
,常配合宏PHP_EVAL_LIBLINE
使用。
dnl
dnl PHP_EVAL_LIBLINE(libline, SHARED-LIBADD)
dnl
dnl Use this macro, if you need to add libraries and or library search
dnl paths to the PHP build system which are only given in compiler
dnl notation.
dnl
下面是简单的例子。
LIBFOO_INCLINE=`pkg-config libfoo --cflags`
LIBFOO_LIBLINE=`pkg-config libfoo --libs`
PHP_EVAL_INCLINE($LIBFOO_INCLINE)
PHP_EVAL_LIBLINE($LIBFOO_LIBLINE, FOO_SHARED_LIBADD)
PHP_SUBST(FOO_SHARED_LIBADD)
注意,最后一定要调用PHP_SUBST
,否则shared-libadd
中的库将不会被连接。
PHP_REQUIRE_CXX
,否则无法编译。AC_DEFINE(ENABLE_BAR, 1, [ ])
替代$CFLAGS
中的-DENABLE_BAR=1
。AC_MSG_ERROR
输出错误信息并中止构建。我们在1.2.1中生成的骨架中包含了php_foo.h和foo.c两个文件。通过阅读源码,可以发现它包含了扩展的入口变量,以及一个简单的函数confirm_foo_compiled()
。在实际开发中,我们可以根据项目需求编写任意数量的源文件和头文件,并在config.m4中正确配置。
每个PHP扩展都有且仅有一个入口,即一个类型为zend_module_entry
的结构体全局变量。它是必不可少的。
在zend_modules.h中,它的定义如下。我在代码中加上了注释,
struct _zend_module_entry {
// ...
// 以上结构的含义不重要,略去。
// 初始化入口变量时使用宏STANDARD_MODULE_HEADER即可
const char *name; // 扩展的名称
const struct _zend_function_entry *functions; // 扩展包含的函数列表入口
int (*module_startup_func)(INIT_FUNC_ARGS); // 扩展启动时执行的函数
int (*module_shutdown_func)(SHUTDOWN_FUNC_ARGS); // 扩展结束运行时执行的函数
int (*request_startup_func)(INIT_FUNC_ARGS); // 每次收到请求时执行的函数
int (*request_shutdown_func)(SHUTDOWN_FUNC_ARGS); // 每次请求结束时执行的函数
void (*info_func)(ZEND_MODULE_INFO_FUNC_ARGS); // `php -i`时执行的函数,输出信息
const char *version; // 扩展的版本号
// ...
// 以下结构的含义不重要,略去
// 初始化入口变量时使用宏STANDARD_MODULE_PROPERTIES即可
};
有关以上“请求”的概念,如果不了解,可以阅读这篇讲PHP生命周期的文章。
通过阅读源码,大家可以发现,绝大多数供PHP扩展开发者使用的Zend API都是宏。大家应该逐渐适应这一点。很多初学者在入门一个新的框架/库的时候,总是偏爱IDE的自动补全功能,在候选函数列表里找到自己想要调用的函数,看到它接受的参数类型及含义,阅读它的API文档。然而,多数IDE对宏的支持并不好,再加上宏参数不具备类型、Zend API没有文档,对于这些初学者来说,确实增加了他们上手的难度。
tests目录下的phpt测试脚本可以用来测试你的PHP扩展是否能够按照预期运行。
在编译完成后,make test
以执行所有phpt脚本。
PHP官网的这篇文章详细地介绍了如何编写phpt脚本,这里就不再赘述了。
在这篇文章中大家主要了解了一个PHP扩展的基本结构,以及如何为自己的PHP扩展编写配置脚本。下次,我将会为大家带来第二章:
- 浅析ZVAL
敬请期待。此外,如果我的文章有纰漏或者有需要补充的地方,欢迎评论指出,或者给我发邮件。
Living on the bleeding edge
Original url: Access
Created at: 2018-10-10 17:15:28
Category: default
Tags: none
未标明原创文章均为采集,版权归作者所有,转载无需和我联系,请注明原出处,南摩阿彌陀佛,知识,不只知道,要得到
最新评论