首页 > phpPHP7扩展开发教程[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这个符号并调用它得到我们的扩展定义信息。


ZEND_DLEXPORT zend_module_entry *get_module() {
    return &module;
}

ZEND_DLEXPORT zend_module_entry *get_module() {
    return &module;
}
zend_module_entry定义了扩展的各种信息,可以在php源码的Zend/zend_modules.h中找到:


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;
};

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;
};
字段好多,但是填充它的代码要短的多:


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,
};

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字段为之:


STANDARD_MODULE_HEADER_EX

STANDARD_MODULE_HEADER_EX
这个宏填充了结构体后面那些并不重要的字段:


STANDARD_MODULE_PROPERTIES

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个时机。


int extension_startup(int type, int module_number) {
    TRACE("extension_startup");
    return SUCCESS;
}

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函数:


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();
}

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的时候包含进来即可:


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_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扩展配置进去:


extension=/path/to/your/myext.so

extension=/path/to/your/myext.so
然后执行php -m | grep myext确认扩展已成功加载:


$ 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|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。