1. 我的第一个 PHP 扩展

1.1 作者的话

1.1.1 为什么要开这个专栏

这个专栏的主要目的是带领大家理解PHP的底层机制,并掌握PHP扩展开发的基本要领。你会发现,在漫山遍野的PHP相关书籍、教程中,这样的内容寥寥无几。业界元老Sara Golemon女士曾写过一本书,Extending and Embedding PHP,于2006年出版。Zend API在每一个版本都有变化,以至于如今这本书已经没有太大参考价值了。目前,基本上只有PHP Internals BookPHP Internals两个非官方站点提供稍微全面些的PHP7扩展开发的指导。然而,它们只起到了官方没有提供的API文档的作用(是的,除了几行少得可怜的注释,官方并没有提供任何API文档),在实际开发中,很多都需要自己摸索,在不断地调试、阅读PHP源码后,问题才能迎刃而解。所以我写这个专栏,除了讲一些基础之外,我还会把我在做PHP扩展开发的过程中踩过的坑分享给大家,让大家少走弯路,能更快上手。毕竟优雅高效开发才是PHPer们所追求的目标。

1.1.2 什么时候我们需要写PHP扩展

在PHP7的年代,userland PHP的性能是足够的。很多时候我们遇到的性能瓶颈都是出在I/O或者业务逻辑上,而不是PHP本身的执行速度不够。而像计算密集的程序,比如一些算法,我们不会拿PHP去做。

那什么时候我们需要写PHP扩展呢?

  • 当PHP的语法特性无法满足我们的要求时。比如在PHP5.5之前,没有Generator,所以如果我们想要在PHP中使用协程,就必须在底层实现一个上下文切换的库
  • 当我们需要使用一个C/C++的库时。只有当你充分阅读并理解它的源码以后你才有可能用PHP重写这个库,而直接封装成PHP扩展你只往往需要理解它暴露的接口就可以了。简单高效。
  • 当PHP的执行速度真的成为我们项目的性能瓶颈时。yaf和swoole等扩展的存在证明了这一点。

最重要的一点,掌握PHP扩展开发的技术,可以给PHP带来无限的可能,而不是局限于Web开发的小领域中。

1.1.3 学习PHP扩展开发需要掌握什么基础

阅读本专栏文章需要掌握以下基础。

  • 了解PHP的基本语法
  • 掌握C/C++基础
  • 掌握使用GDB调试C/C++程序的基本方法

之后我写文章时默认大家有这样的基础。不然事无巨细,连malloc是什么都要展开讲,对于那些有基础的读者来说可谓是一种折磨。

1.2 PHP扩展的文件结构

1.2.1 生成一个PHP扩展的骨架

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 tests

foo/tests:
001.phpt

1.2.2 config.m4脚本

我们现在可以看到生成的config.m4脚本。这个脚本在PHP扩展中至关重要,它告诉PHP构建系统应该如何构建这个扩展。我们可以调用acinclude.m4中定义的M4宏来方便我们编写配置脚本。acinclude.m4有详细的注释,所以不难理解。大家也可以阅读官方扩展以及PECL扩展的config.m4脚本来熟悉一下写法。下面我会讲解几个最常用的宏的使用方法。

1.2.2.1 ./configure参数

我们知道,当我们执行phpize后,PHP编译系统将使用autoconf,根据config.m4生成configure脚本。我们往往希望在执行configure脚本时指定某些特定参数,比如--enable-pcntl--with-curl=/usr/local。我们可以在config.m4中调用PHP_ARG_ENABLEPHP_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-notyes,则该参数表示这个扩展本身是否会被编译。一般来说,一个扩展有且仅有一个这样的参数,且它的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_FOOyes,而--disable-foo会置no。如果是--enable-foo=bar,那它就和with等价,置$PHP_FOO为等号后的字符串。注意,不论arg-name为如何,这个对应的变量永远是大写,而且"-"会被转换为"_"。

1.2.2.2 添加扩展

使用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

cxxyes表明在link时libtool会选择g++而不是cc,如果你的扩展是用C++编写的,建议设置为yes,否则你需要-lstdc++(除非你的依赖包含了其他C++库,已经连接了libstdc++)。

zend-ext若为yes,则表明这是一个Zend扩展而不是PHP扩展。在一般的应用场合,PHP扩展就足以满足我们的要求。而且编写Zend扩展要求开发者对Zend引擎有着深刻的理解。我们短时间内不做讨论。

1.2.2.3 配置依赖

我们的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中的库将不会被连接。

1.2.2.4 其他

  • 如果你的扩展是使用C++编写的,必须调用宏PHP_REQUIRE_CXX,否则无法编译。
  • 常使用AC_DEFINE(ENABLE_BAR, 1, [ ])替代$CFLAGS中的-DENABLE_BAR=1
  • 如果在配置过程中检测到错误,编译不应该继续进行下去,可以调用宏AC_MSG_ERROR输出错误信息并中止构建。
  • config.w32脚本包含该扩展在Windows下构建时的配置信息,这里我们不做讨论。

1.2.3 源文件

我们在1.2.1中生成的骨架中包含了php_foo.h和foo.c两个文件。通过阅读源码,可以发现它包含了扩展的入口变量,以及一个简单的函数confirm_foo_compiled()。在实际开发中,我们可以根据项目需求编写任意数量的源文件和头文件,并在config.m4中正确配置。

1.2.3.1 PHP扩展入口

每个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生命周期的文章

1.2.3.2 使用宏进行PHP扩展开发

通过阅读源码,大家可以发现,绝大多数供PHP扩展开发者使用的Zend API都是宏。大家应该逐渐适应这一点。很多初学者在入门一个新的框架/库的时候,总是偏爱IDE的自动补全功能,在候选函数列表里找到自己想要调用的函数,看到它接受的参数类型及含义,阅读它的API文档。然而,多数IDE对宏的支持并不好,再加上宏参数不具备类型、Zend API没有文档,对于这些初学者来说,确实增加了他们上手的难度。

1.2.4 测试脚本

tests目录下的phpt测试脚本可以用来测试你的PHP扩展是否能够按照预期运行。

在编译完成后,make test以执行所有phpt脚本。

PHP官网的这篇文章详细地介绍了如何编写phpt脚本,这里就不再赘述了。

1.3 下期预告

在这篇文章中大家主要了解了一个PHP扩展的基本结构,以及如何为自己的PHP扩展编写配置脚本。下次,我将会为大家带来第二章:

  1. 浅析ZVAL

敬请期待。此外,如果我的文章有纰漏或者有需要补充的地方,欢迎评论指出,或者给我发邮件。

Living on the bleeding edge

本帖由系统于 1年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 3

很强势

1年前 评论

折腾折腾 ?

1年前 评论
lmaster

666

1年前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!