PHP7扩展开发教程[1] – 怎样导出一个模块?
受PHP-X项目启发,我决定在未来的一段时间编写一系列php7扩展开发教程,一方面是沉淀最近一段时间的php7扩展开发知识,另外也可以将学习成果贡献给更多需要参与到php7扩展开发中的有志之士们。
在编写该系列博客时,我力求每个章节的功能点高度聚焦,代码保持短小,将需要了解的基础知识点介绍给大家,同时也为大家自行探索更多丰富的php api提供必要的线索,”授人以渔”是该系列博客的初衷。
每一个章节的代码将维护在github项目:php7-extension-explore,后续会随着教程的逐步编写不断丰富,最后会考虑将博客的内容整理到gitbook,但暂时仍旧在wordpress中撰写与提供查阅。
- 本博客假设你熟悉c和php开发,因此将不对语言细节、搭建方法做深入的讲解。
- 尽量避免使用Zend的宏定义,展示原生API和数据结构,方便大家深入理解。
- 只运行在linux+nginx+php-fpm环境下,线程安全之类的问题完全不涉及。
正式开始
本章节会搭建第一个php7扩展叫做myext,最终的目标是编译出一个扩展myext.so,并令php加载扩展,同时可以在phpinfo()中看到扩展的一些说明信息。
源代码请打开:https://github.com/owenliang/php7-extension-explore/tree/master/course1-how-to-export-a-module
myext.h
头文件myext.h中引入了php.h,它包含了我们常用的php扩展API定义。另外一个头文件是ext/standard/info.h,ext是php的扩展目录,standard是扩展的名字,我们的扩展会用到这个扩展里的方法。
TRACE宏定义是为了调试扩展临时定义的日志函数,它会打印代码位置和信息到屏幕,便于我们调试追踪。
myext.c
扩展编译产生so,php/php-fpm程序会通过dlopen加载我们的myext.so扩展,然后通过dlsym找到get_module这个符号并调用它得到我们的扩展定义信息。
1 2 3 |
ZEND_DLEXPORT zend_module_entry *get_module() { return &module; } |
zend_module_entry定义了扩展的各种信息,可以在php源码的Zend/zend_modules.h中找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
struct _zend_module_entry { unsigned short size; unsigned int zend_api; unsigned char zend_debug; unsigned char zts; const struct _zend_ini_entry *ini_entry; const struct _zend_module_dep *deps; 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); const char *version; size_t globals_size; #ifdef ZTS ts_rsrc_id* globals_id_ptr; #else void* globals_ptr; #endif void (*globals_ctor)(void *global); void (*globals_dtor)(void *global); int (*post_deactivate_func)(void); int module_started; unsigned char type; void *handle; int module_number; const char *build_id; }; |
字段好多,但是填充它的代码要短的多:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
zend_module_entry module = { STANDARD_MODULE_HEADER_EX, // size,zend_api,zend_debug,zts NULL, // ini_entry NULL, // deps "myext", //name NULL, // functions extension_startup, // module_startup_func extension_shutdown, // module_shutdown_func extension_before_request, // request_startup_func extension_after_request, // request_shutdown_func extension_info, // info_func "1.0", // version // globals_size,globals_ptr,globals_ctor,globals_dtor,post_deactivate_func,module_started,type, // handle,module_number,build_id STANDARD_MODULE_PROPERTIES, }; |
这个宏填充了结构体前面那些并不重要的字段,直到ini_entry字段为之:
1 |
STANDARD_MODULE_HEADER_EX |
这个宏填充了结构体后面那些并不重要的字段:
1 |
STANDARD_MODULE_PROPERTIES |
至于这2个宏填充了哪些字段都已经注释说明,目前还没有具体使用过这些字段。
在目前使用到的字段中,name字段是模块的名称,version是模块的版本,其他字段后面的章节再慢慢涉及。
本章要重点关注的是extension_系列函数,它们涉及到扩展的生命期概念。
module_startup_function和module_shutdown_function分别在扩展加载和销毁时回调,对CGI/FPM都是一样的,都是进程启动调用一次,进程退出调用一次。
request_startup_function和request_shutdown_function分别在请求处理前和处理后回调,对于CGI来说就是启动和退出的2个时机,对于FPM来说是一个fastcgi请求处理前和处理后2个时机。
1 2 3 4 |
int extension_startup(int type, int module_number) { TRACE("extension_startup"); return SUCCESS; } |
以extension_startup函数为例,它的参数是type和module_number,其他3个函数也是一样的。
module_number是运行时分配的一个扩展唯一ID,后面章节会用得到。type表示扩展是持久的还是临时的,我们通过php.ini配置加载的扩展都是持久的,而在PHP代码里通过dl函数加载的叫做临时的,所以type对我们一般没有用处。
再看一下无关痛痒的extensin_info函数:
1 2 3 4 5 6 7 |
void extension_info(zend_module_entry *zend_module) { php_info_print_table_start(); php_info_print_table_header(2, "myext support", "enabled"); php_info_print_table_row(2, "author", "owenliang"); php_info_print_table_row(2, "course name", "course1-how-to-export-a-module"); php_info_print_table_end(); } |
这个函数可以在php的phpinfo()中输出扩展的一些html描述信息,php_info_print_….这些api来自于ext/standard扩展,而这个扩展是php默认会安装的,你就不要担心这些函数符号找不到了。
这些函数的实现你可以在ext/standard/info.c中找到,这些函数符号存在于php的二进制中,myext扩展在被php加载时可以自动在php中找到符号并完成调用,你大可放心。
makefile
理解makefile非常重要,我们利用php-config程序获得php的头文件和库文件的所在位置,在编译扩展myext.so的时候包含进来即可:
1 2 3 4 5 |
PHP_INCLUDE = `php-config --includes` PHP_LIBS = `php-config --libs` PHP_LDFLAGS = `php-config --ldflags` PHP_INCLUDE_DIR = `php-config --include-dir` PHP_EXTENSION_DIR = `php-config --extension-dir` |
你可以运行php-config系列命令,看看这些变量具体的内容。
最终我们将myext.c编译成myext.so,通过make && make install便会自动安装到PHP_EXTENSION_DIR目录下了。
为了加载so,我们根据php –ini找到php.ini的所在位置,修改它将myext.so扩展配置进去:
1 |
extension=/path/to/your/myext.so |
然后执行php -m | grep myext确认扩展已成功加载:
1 2 3 4 5 6 |
$ php -m|grep myext extension_startup(myext.c:4) - extension_startup extension_before_request(myext.c:14) - extension_before_request extension_after_request(myext.c:19) - extension_after_request myext extension_shutdown(myext.c:9) - extension_shutdown |
正常情况下会看到这些信息,因为php -m 属于CLI执行,因此它会经历完整的php生命期,程序里打印的TRACE日志都得以输出。
你也可以通过make test来重复测试,它相当于执行php test.php。
结语
通过本章,掌握:
- 导出扩展
- 编译扩展
- 加载扩展
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

收藏了~
多谢支持~
ZEND_DLEXPORT 这个是一个宏还是啥
这是宏,你可以找到定义看一下,我的教程里尽量是展示原本面貌,避免用宏。
可否 全部标注一遍 对应的宏。 即用代码实现一遍也 备注一下对应的宏。
不错的建议,不过我暂时工作重心有所转移,等我再搞扩展的时候争取补充宏的信息。