[翻译][php扩展开发和嵌入式]第12章-php的启动过程

全部翻译内容pdf文档下载地址: http://download.csdn.net/detail/lgg201/5107012

本书目前在github上由laruence(http://www.laruence.com)和walu(http://www.walu.cc)两位大牛组织翻译. 该翻译项目地址为: https://github.com/walu/phpbook

本书在github上的地址: https://github.com/goosman-lei/php-eae

未来本书将可能部分合并到phpbook项目中, 同时保留一份独立版本.


原书名: <Extending and Embedding PHP>

原作者: Sara Golemon

译者: goosman.lei(雷果国)

译者Email: [email protected]

译者Blog: http://blog.csdn.net/lgg201

权利声明

此译本在不获利的情况下, 可以无限制自由传播.

启动, 终止, 以及其中的一些点

在本书中, 你已经多次使用MINIT函数在php加载你扩展的共享库时执行初始化任务. 在第1章"php的生命周期"中, 你还学习了其他三个启动/终止函数, 与MINIT对应的是MSHUTDOWN, 另外还有一对RINIT/RSHUTDOWN方法在每个页面请求启动和终止时被调用.

生命周期

除了这四个直接链接到模块结构的函数外, 还有两个函数仅用于线程环境, 用来处理每个线程的启动和终止, 以及它们使用的似有存储空间.开始之前, 首先将你的php扩展骨架程序拷贝到php源码树的ext/sample4下. 代码如下

config.m4

PHP_ARG_ENABLE(sample4,
  [Whether to enable the "sample4" extension],
  [  enable-sample4       Enable "sample4" extension support])

if test $PHP_SAMPLE4 != "no"; then
  PHP_SUBST(SAMPLE4_SHARED_LIBADD)
  PHP_NEW_EXTENSION(sample4, sample4.c, $ext_shared)
fi

php_sample4.h

#ifndef PHP_SAMPLE4_H
/* Prevent double inclusion */
#define PHP_SAMPLE4_H

/* Define Extension Properties */
#define PHP_SAMPLE4_EXTNAME    "sample4"
#define PHP_SAMPLE4_EXTVER    "1.0"

/* Import configure options
   when building outside of
   the PHP source tree */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

/* Include PHP Standard Header */
#include "php.h"

/* Define the entry point symbol
 * Zend will use when loading this module
 */
extern zend_module_entry sample4_module_entry;
#define phpext_sample4_ptr &sample4_module_entry

#endif /* PHP_SAMPLE4_H */

sample4.c

#include "php_sample4.h"
#include "ext/standard/info.h"

static function_entry php_sample4_functions[] = {
    { NULL, NULL, NULL }
};

PHP_MINIT_FUNCTION(sample4)
{
    return SUCCESS;
}

PHP_MSHUTDOWN_FUNCTION(sample4)
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION(sample4)
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION(sample4)
{
    return SUCCESS;
}
PHP_MINFO_FUNCTION(sample4)
{
}

zend_module_entry sample4_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    PHP_SAMPLE4_EXTNAME,
    php_sample4_functions,
    PHP_MINIT(sample4),
    PHP_MSHUTDOWN(sample4),
    PHP_RINIT(sample4),
    PHP_RSHUTDOWN(sample4),
    PHP_MINFO(sample4),
#if ZEND_MODULE_API_NO >= 20010901
    PHP_SAMPLE4_EXTVER,
#endif
    STANDARD_MODULE_PROPERTIES
};

#ifdef COMPILE_DL_SAMPLE4
ZEND_GET_MODULE(sample4)
#endif

要注意, 每个启动和终止函数在退出时都返回SUCCESS. 如果这些函数中某个返回FAILURE, 引擎就认为这个过程失败并中断php的执行.

模块生命周期

在前面章节已经多次使用, 因此MINIT对你来说应该已经很熟悉了. 它在模块第一次加载到进程空间时触发, 对于单请求sapi比如CLI和CGI, 或者多线程sapi比如apache2-worker, 它都只执行一次, 因为不涉及到fork.

对于多进程sapi, 比如apache1, apache2-prefork, 通过它们的mod_php实例for了多个webserver进程. 每个mod_php实例都必须加载自己的扩展模块, 因此MINIT将被执行多次, 不过对于每个进程, 它仍然只执行一次.

当模块被卸载时, MSHUTDOWN方法被调用, 此时模块的所有资源(比如持久化内存块)都将被释放, 返回给操作系统.

引擎端的特性, 比如类, 资源ID, 流包装和过滤器, 用户空间全局变量, php.ini中的指令这些公共的资源都是在模块的INIT和SHUTDOWN阶段被分配和释放的.

理论上来说, 你可以不用在MSHUTDOWN阶段做资源释放的工作, 把它留给OS去做隐式的内存和文件释放. 不过在apache 1.3中使用你的扩展时, 你会发现一个有趣的现象, apache将加载mod_php, 在进程中运行MINIT, 接着立即卸载mod_php, 触发MSHUTDOWN方法, 接着再次加载它. 如果没有正确的MSHUTDOWN阶段, 在MINIT阶段初始分配的资源就将泄露.

线程生命周期

在多线程sapi中, 有时需要为每个线程分配它自己独立的资源, 或跟踪它自己的单请求计数器.对于这些特殊情况, 存在一组每个线程的钩子, 允许在线程启动和终止时执行它们. 典型的情况是当apache2-worker这样的sapi启动时, 它将会产生一打或更多的线程去处理并发请求.

任何在多请求间共享, 在同一进程中不同线程有不能访问的资源, 都是在线程的构造器和析构器中分配和释放的. 比如这可能包括EG(persistent_list)HashTable中的持久化资源, 因为它们通常包括网络或文件资源, 需要考虑指令间它们的状态一致性.

请求生命周期

最后一个也是最短的生命周期是请求生命周期, 在这个周期内, 你的扩展可能会去初始化默认的用户空间变量, 或初始化内部状态跟踪信息. 因为这些方法在每个页面请求都被调用, 因此要尽可能的保证这些处理和内存分配可以执行的足够快.

通过MINFO对外暴露模块信息

除非你计划只有很少人使用你的扩展, 并且并没有计划修改API, 否则你就需要能够告诉用户空间一些关于扩展自身的信息. 比如, 是否所有的环境和版本特有特性都可用? 它编译的外部库的版本是什么? 是否有网站或邮件地址可以让你扩展的用户在需要时寻求帮助?

如果你曾经看过phpinfo()或php -i的输出, 你就会注意到, 所有这些信息都被组织到一种良好格式, 易于解析的输出中. 你的扩展可以很简单的在这些内容中增加自己的信息, 只需要在你的模块中增加一个MINFO()函数即可:

PHP_MINFO_FUNCTION(sample4)
{
    php_info_print_table_start();
    php_info_print_table_row(2, "Sample4 Module", "enabled");
    php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
    php_info_print_table_end();
}

通过使用这些包装函数, 你的模块信息将在从webserver sapi(比如cgi, iis, apache等)输出时自动的包装为HTML标签, 而在使用cli时输出为普通文本. 为了使得构建时你的扩展中可以使用这些函数原型, 你需要#include "ext/standard/info.h".

下面是这个头文件中可用的php_info_*()族函数.

  • char *php_info_html_esc(char *str TSRMLS_DC)

用户空间htmlentites()函数的底层实现php_escape_html_entities()的一个包装. 返回的字符串是用emalloc()分配的, 使用后必须显式的使用efree()释放.

  • void php_info_print_table_start(void)
  • void php_info_print_table_end(void)

输出html表格的开始/结束标签. html输出禁用时, 比如在cli中, 它将在start中输出换行符, end中不输出任何内容.

  • void php_info_print_table_header(int cols, ...)
  • void php_info_print_table_colspan_header(int cols, char *header)

输出一行表头. 第一个版本为每个可变参输出一个<th></th>, 内容是cols后面的字符串参数. clospan版的则只输出一个<th></th>, 并给它指定colspan属性.

  • void php_info_print_table_row(int cols, ...)
  • void php_info_print_table_row_ex(int cols, char *class, ...)

这两个版本都为每个可变参输出一个<td></td>. 两者的不同在于前者将为其设置class="v"属性, 而后者则允许调用者指定自己的类名用于自定义格式. 没有打开HTML格式输出时, 由于只是文本输出, 两者的差异就不复存在了.

  • void php_info_print_box_start(int flag)
  • void php_info_print_box_end()

这两个函数只是简单的输出一个表格(<tr class="h"><td>, </td></tr>)的开始和结束. 如果给定的flag值非0, 则使用class="h", 否则使用class="v". 使用非html输出时, 标记为0将导致在star中输出一个换行符, 此时这两个函数不会在产生其他任何输出.

  • void php_info_print_hr(void)

这个函数在html启用时输出<hr />标签, 或者, 当没有启用html输出时, 输出31个下划线, 并在前后各输出两个换行符.

在MINFO中通常可以使用PHPWRITE()和php_printf(), 但手动输出内容时应该注意它需要依赖于当前的SAPI期望输出文本还是html. 可以通过测试全局的sapi_module结构体的phpinfo_as_text属性来确认这一点:

PHP_MINFO_FUNCTION(sample4)
{
    php_info_print_table_start();
    php_info_print_table_row(2, "Sample4 Module", "enabled");
    php_info_print_table_row(2, "version", PHP_SAMPLE4_EXTVER);
    if (sapi_module.phpinfo_as_text) {
        /* No HTML for you */
        php_info_print_table_row(2, "By",
            "Example Technologies\nhttp://www.example.com");
    } else {
        /* HTMLified version */
        php_printf("<tr>"
            "<td class=\"v\">By</td>"
            "<td class=\"v\">"
            "<a href=\"http://www.example.com\""
            " alt=\"Example Technologies\">"
            "<img src=\"http://www.example.com/logo.png\" />"

            "</a></td></tr>");
    }
    php_info_print_table_end();
}

常量

向用户空间脚本暴露信息更好的方法是使用扩展定义脚本可以在运行时访问的常量, 并可以通过这些常量改变扩展的某些行为. 在用户空间中, 我们使用define()函数定义常量; 内部, 则是和它非常相似的REGISTER_*_CONSTANT()一族的宏.

多数常量是你想要它们在所有脚本中初始化为相同值的数据. 它们是在MINIT函数中定义的.

PHP_MINIT_FUNCTION(sample4)
{
    REGISTER_STRING_CONSTANT("SAMPLE4_VERSION",
            PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT);

    return SUCCESS;
}

这个宏的第一个参数是要暴露给用户空间的常量名. 在这个例子中, 用户空间就可以执行echo SAMPLE4_VERSION; 得到输出1.0. 这里有一点要特别注意, REGISTER_*_CONSTANT()一族的宏使用了sizeof()调用去确定常量名的长度. 也就是说只能使用字面量值. 如果使用char *变量则会导致不正确的结果(sizeof(char *)在32位平台上通常是4, 而不是真正字符串的长度).

下一个参数是常量的值. 多数情况下, 它只需要一个参数, 不过, 对于STRINGL版本, 你还需要一个参数去指定长度. 在注册字符串常量时, 字符串的值并不会拷贝到常量中, 只是引用它. 也就是说需要在持久化内存中为其分配空间, 并在对应的SHUTDOWN阶段释放它们.

最后一个参数是一个有两个可选值的位域操作结果. CONST_CS标记说明该常量大小写敏感. 对于用户空间定义的常量以及几乎所有的内部常量来说, 这都是默认行为. 只有极少数的情况, 比如trUE, FALSE, NULL, 在注册时省略了这个标记用以说明它们是不区分大小写的.

注册常量时的第二个标记是持久化标记. 当在MINIT中定义常量时, 它们必须被构建为跨请求的持久化常量. 但是, 如果在请求中定义常量, 比如在RINIT中, 你可能就需要省略这个标记以允许引擎在请求结束时销毁该常量了.

下面是4个可用的常量注册宏的原型. 一定要记住, 名字参数必须是字符串字面量而不能是char *变量:

REGISTER_LONG_CONSTANT(char *name, long lval, int flags)
REGISTER_DOUBLE_CONSTANT(char *name, double dval, int flags)
REGISTER_STRING_CONSTANT(char *name, char *value, int flags)
REGISTER_STRINGL_CONSTANT(char *name,
                     char *value, int value_len, int flags)

如果字符串必须从变量名初始化, 比如在循环中, 你可以使用如下的函数调用(上面的宏就是映射到这些函数中的):

void zend_register_long_constant(char *name, uint name_len,
        long lval, int flags, int module_number TSRMLS_DC)
void zend_register_double_constant(char *name, uint name_len,
        double dval, int flags, int module_number TSRMLS_DC)
void zend_register_string_constant(char *name, uint name_len,
        char *strval, int flags, int module_number TSRMLS_DC)
void zend_register_stringl_constant(char *name, uint name_len,
        char *strval, uint strlen, int flags,
        int module_number TSRMLS_DC)

此时, 名字参数的长度可以直接由调用作用域提供. 你应该注意到, 这次就必须显式的传递TSRMLS_CC参数了, 并且, 这里还引入了另外一个参数.

module_number是在你的扩展被加载或被卸载时传递给你的信息. 你不用关心它的值, 只需要传递它就可以了. 在MINIT和RINIT函数原型中都提供了它, 因此, 在你定义常量的时候, 它就是可用的. 下面是函数版的常量注册例子:

PHP_MINIT_FUNCTION(sample4)
{
    register_string_constant("SAMPLE4_VERSION",
        sizeof("SAMPLE4_VERSION"),
        PHP_SAMPLE4_EXTVER,
        CONST_CS | CONST_PERSISTENT,
        module_number TSRMLS_CC);

    return SUCCESS;
}

要注意当sizeof()用于确定SAMPLE4_VERSION的长度时, 这里并没有减1. 常量的名字是包含它的终止NULL的. 如果使用strlen()确定长度, 要记得给结果加1以使其包含终止的NULL.

除了数组和对象, 其他的类型都可以被注册, 但是因为在ZEND API中不存在这些类型的宏或函数, 你就需要手动的定义常量. 按照下面的范本, 仅需要在使用时修改去创建恰当类型的zval *即可:

void php_sample4_register_boolean_constant(char *name, uint len,
    zend_bool bval, int flags, int module_number TSRMLS_DC)
{
    zend_constant c;

    ZVAL_BOOL(&c.value, bval);
    c.flags = CONST_CS | CONST_PERSISTENT;
    c.name = zend_strndup(name, len - 1);
    c.name_len = len;
    c.module_number = module_number;
    zend_register_constant(&c TSRMLS_CC);
}

扩展的全局空间

如果可以保证任何时刻一个进程中只有一个php脚本在执行, 你的扩展就可以随意的定义全局变量并去访问它们, 因为已知在opcode执行过程中不会有其他脚本被执行. 对于非线程sapi, 这是可行的, 因为所有的进程空间中都只能同时执行一个代码路径.

然而在线程sapi中, 可能会有两个或更多的线程同时读或更糟糕的情况是同时写相同的值. 为了解决这个问题, 就引入了一个扩展的全局空间概念, 它为每个扩展的数据提供一个唯一的数据存储桶.

定义扩展的全局空间

要给你的扩展申请一块存储的桶, 首先就需要在php_sample4.h上的一个标准结构体中定义所有你的全局变量. 比如, 假设你的扩展要保存一个计数器, 保持对某个方法在请求内被调用次数的跟踪, 你就需要定义一个结构体包含一个unsigned long:

ZEND_BEGIN_MODULE_GLOBALS(sample4)
    unsigned long counter;
ZEND_END_MODULE_GLOBALS(sample4)

ZEND_BEGIN_MODULE_GLOBALS和ZEND_END_MODULE_GLOBALS宏为扩展全局变量结构的定义提供了统一的框架. 如果你看过这个块的展开形式, 就可以很容易的理解它了:

typedef struct _zend_sample4_globals {
    unsigned long counter;
} zend_sample4_globals;

你可以像在其他的C语言结构体中增加成员一样, 为它增加其他成员. 现在, 你有了存储桶的(数据结构)定义, 接下来要做的就是声明一个这个类型的变量, 你需要在扩展的sample4.c文件中, #include "php_sample4.h"语句下一行声明它:

ZEND_DECLARE_MODULE_GLOBALS(sample4);

它将根据是否启用了线程安全, 被解析为两种不同的格式. 对于非线程安全构建, 比如apache1, apache2-prefork, cgi, cli以等等, 它是直接在真正的全局作用域声明了一个zend_sample4_globals结构体的直接值:

zend_sample4_globals sample4_globals;

这和你在其他单线程应用中声明的全局变量没有什么差异. 计数器的值直接通过sample4_globals.counter访问. 而对于线程安全构建, 则是另外一种处理, 它只是声明了一个整型值, 以后它将扮演到真实数据的引用的角色:

int sample4_globals_id;

设置这个ID就代表声明你的扩展全局变量到引擎中. 通过提供的信息, 引擎将在每个新的线程产生时分配一块内存 专门用于线程服务请求时的似有存储空间. 在你的MINIT函数中增加下面的代码块:

#ifdef ZTS
    ts_allocate_id(&sample4_globals_id,
                sizeof(zend_sample4_globals),
                NULL, NULL);
#endif

注意, 这个语句被包裹在一个ifdef中, 以放置在没有启用Zend线程安全(ZTS)时执行它. 这是因为sample4_globals_id只在线程环境下才会被声明, 非线程环境的构建则使用的是sample4_globals变量的直接值.

每个线程的初始化和终止

在非线程构建中, 你的zend_sample4_globals结构体在一个进程中只有一份拷贝. 你可以给它设置初始值或在MINIT或RINIT中为其分配资源, 进行初始化, 在MSHUTDOWN和RSHUTDOWN阶段如果需要, 则进行相应的释放.

然而, 对于线程构建, 每次一个新的线程产生时, 都会分配一个新的结构体. 实际上, 这在webserver启动时可能会发生很多次, 而在webserver进程的整个生命周期中, 这可能会发生成百上千次. 为了知道怎样初始化和终止你的扩展全局空间, 引擎需要执行一些回调函数. 这就是上面的例子中你传递给ts_allocate_id()的NULL参数; 在你的MINIT函数上面增加下面的两个函数:

static void php_sample4_globals_ctor(
            zend_sample4_globals *sample4_globals TSRMLS_DC)
{
    /* 在线程产生时初始化一个新的zend_sample4_globals结构体 */
    sample4_globals->counter = 0;
}
static void php_sample4_globals_dtor(
            zend_sample4_globals *sample4_globals TSRMLS_DC)
{
    /* 在初始化阶段分配的各种资源, 都在这里释放 */
}

接着, 在启动和终止时使用这些函数:

PHP_MINIT_FUNCTION(sample4)
{
    REGISTER_STRING_CONSTANT("SAMPLE4_VERSION",
            PHP_SAMPLE4_EXTVER, CONST_CS | CONST_PERSISTENT);
#ifdef ZTS
    ts_allocate_id(&sample4_globals_id,
                sizeof(zend_sample4_globals),
                (ts_allocate_ctor)php_sample4_globals_ctor,
                (ts_allocate_dtor)php_sample4_globals_dtor);
#else
    php_sample4_globals_ctor(&sample4_globals TSRMLS_CC);
#endif
    return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(sample4)
{
#ifndef ZTS
    php_sample4_globals_dtor(&sample4_globals TSRMLS_CC);
#endif
    return SUCCESS;
}

要注意, 在没有开启ZTS时, ctor和dtor函数是手动调用的. 不要忘记: 非线程环境也需要初始化和终止.

你可能奇怪为什么在php_sample4_globals_ctor()和php_sample4_globals_dtor()中直接使用了TSRMLS_CC. 如果你认为"这完全不需要, 它在ZTS禁用时解析出来是空的内容, 并且由#ifndef指令, 我们知道ZTS是被禁用的, 你的观点绝对正确. 声明中的相关的TSRMLS_DC指令仅用于保证代码的一致性. 从积极的一面考虑, 如果ZEND API修改这些值使得在非ZTS构建中也有有效内容时, 你的代码就不需要修改就做好了相应的调整.

访问扩展的全局空间

现在你的扩展有了一个全局变量集合, 你可以开始在你的代码中访问它们了. 在非ZTS模式中这很简单, 只需要访问进程全局作用域的sample4_globals变量的相关成员即可, 比如, 下面的用户空间函数增加了你前面定义的计数器并返回它的当前值:

PHP_FUNCTION(sample4_counter)
{
    RETURN_LONG(++sample4_globals.counter);
}
很简单很容易. 不幸的是, 这种方式在线程环境的PHP构建中不能工作. 这种情况下你就需要做更多的工作. 下面是使用ZTS语义的该函数返回语句:
RETURN_LONG(++TSRMG(sample4_globals_id,
                zend_sample4_globals*, counter));

TSRMG()宏需要你已经传递的TSRMLS_CC参数, 它会从当前线程池的资源结构中查找需要的数据. 这里, 它使用sample4_globals_id索引映射到内存池中你扩展的全局结构体的位置, 最终, 使用数据类型映射的元素名得到结构体中的偏移量. 因为你并不知道运行时你的扩展是否使用ZTS模式, 因此, 你需要让你的代码适应两种情况. 要做到这一点, 就需要按照下面方式重写该函数:

PHP_FUNCTION(sample4_counter)
{
#ifdef ZTS
    RETURN_LONG(++TSRMG(sample4_globals_id, \
                    zend_sample4_globals*, counter));
#else /* non-ZTS */
    RETURN_LONG(++sample4_globals.counter);
#endif
}

看起来不舒服? 是的, 如果你所有的代码都基于这样的ifdef指令去处理线程安全的全局访问, 它看起来可能比Perl还糟糕! 这就是为什么在所有的PECL扩展中都使用了一个抽象的宏来封装全局访问的原因. 在你的php_sample4.h文件中进行如下定义:

#ifdef ZTS
#include "TSRM.h"
#define SAMPLE4_G(v)    TSRMG(sample4_globals_id,
                         zend_sample4_globals*, v)
#else
#define SAMPLE4_G(v)    (sample4_globals.v)
#endif

这样, 就可以让你访问扩展全局空间时变得简单易懂:

PHP_FUNCTION(sample4_counter)
{
    RETURN_LONG(++SAMPLE4_G(counter));
}

这个宏给你一种似曾相识的感觉吗? 应该是这样的. 它和你已经使用过的EG(symbol_table)以及EG(active_symbol_table)是仙童的概念和实践. 在阅读php源码树中其他部分以及其他扩展时, 你会经常碰到这种宏. 下表列出了常用的全局访问宏:


访问宏

关联数据

EG()

执行全局空间.这个结构体主要用于引擎内部对当前请求的状态跟踪.这个全局空间中可以找到符号表,函数表,类表,常量表,资源表等.

CG()

核心全局空间.主要被Zend引擎在脚本编译和内核底层执行过程中使用.在你的扩展中一般很少直接测试这些值.

PG()

php全局空间.多数"核心的"php.ini指令映射到php全局变量结构体中的一个或多个元素.比如: PG(register_globals), PG(safe_mode)以及PG(memory_limit)

FG()

文件全局空间.多数文件I/O或流相关的全局变量被装入到这个结构通过标准扩展暴露.


用户空间超级全局变量

用户空间有它自己的完全无关的全局概念. 在用户空间, 有一种特殊的全局变量被称为超级全局变量. 这种特殊的用户空间变量包括$_GET, $_POST, $_FILE等等, 在全局作用域, 函数或方法内部都可以等同本地作用域进行访问.

这是由于超级全局变量的解析方式造成的, 它们必须在脚本编译之前定义. 这就意味着在普通的脚本中不能定义其他超级全局变量. 不过, 在扩展中, 可以在请求接收到之前去将变量名定义为超级全局变量.

扩展定义超级全局变量的一个基本示例是ext/session, 它在session_start()和session_write_close()或脚本结束之间, 使用$_SESSION超级全局变量存储会话信息. 为了将$_SESSION定义为超级全局变量, session扩展在MINIT函数中执行了一次下面的语句:

PHP_MINIT_FUNCTION(session)
{
    zend_register_auto_global("_SESSION",
                         sizeof("_SESSION") - 1,
                         NULL TSRMLS_CC);
    return SUCCESS;
}

注意, 第二个参数, 变量名的长度, 使用了sizeof() - 1, 因此不包含终止NULL. 这和之前你看到的多数内部调用不同, 因此, 在定义自己的超级全局变量时要格外小心这一点.

zend_register_auto_global()函数在Zend引擎2中的原型如下:

int zend_register_auto_global(char *name, uint name_len,
    zend_auto_global_callback auto_global_callback TSRMLS_DC)

在Zend引擎1中, auto_global_callback参数并不存在. 为了让你的扩展兼容php4, 就需要在MINIT函数中通过#ifdef块去选择性的执行不同的调用, 定义$_SAMPLE4超级全局变量.

PHP_MINIT_FUNCTION(sample4)
{
    zend_register_auto_global("_SAMPLE4", sizeof("_SAMPLE4") - 1
#ifdef ZEND_ENGINE_2
                        , NULL
#endif
                        TSRMLS_CC);
    return SUCCESS;
}

自动全局回调

ZE2中zend_register_auto_global()的auto_global_callback参数是一个指向自定义函数的指针, 该函数在编译阶段用户空间脚本碰到你的超级全局变量时将被触发. 实际上, 它可以用于在当前脚本没有访问超级全局变量时避免繁杂的初始化处理. 考虑下面的代码:

zend_bool php_sample4_autoglobal_callback(char *name,
                            uint name_len TSRMLS_DC)
{
    zval *sample4_val;
    int i;

    MAKE_STD_ZVAL(sample4_val);
    array_init(sample4_val);
    for(i = 0; i < 10000; i++) {
        add_next_index_long(sample4_val, i);
    }
    ZEND_SET_SYMBOL(&EG(symbol_table), "_SAMPLE4",
                                    sample4_val);
    return 0;
}
PHP_MINIT_FUNCTION(sample4)
{
    zend_register_auto_global("_SAMPLE4", sizeof("_SAMPLE4") - 1
#ifdef ZEND_ENGINE_2
                        , php_sample4_autoglobal_callback
#endif
                        TSRMLS_CC);
    return SUCCESS;
}

php_sample4_autoglobal_callback()所做的工作代表的是对内存和CPU时间的耗费, 如果$_SAMPLE4没有被访问, 则这些资源都将被浪费. 在Zend引擎2中, 只有当脚本被编译时发现某个地方访问了$_SAMPLE4才会调用php_sample4_autoglobal_callback()函数. 注意, 一旦数组初始化完成并增加到请求的符号表后, 函数就返回0值. 这样就解除了请求中后续对该超级全局变量访问时的回调, 以确保对$_SAMPLE4的多次访问不会导致对该回调函数的多次调用. 如果你的扩展需要在每次碰到该超级全局变量时都执行回调函数, 只需要让回调函数返回真值(非0)使得超级全局变量回调函数不被解除即可.

不幸的是, 现在的设计和php4/zend引擎1冲突, 因为旧的引擎并不支持自动全局回调. 这种情况下, 你就需要在每次脚本启动时, 无论是否使用了变量都去初始化. 要这样做, 直接在RINIT函数中调用你上面编写的回调函数即可:

PHP_RINIT_FUNCTION(sample4)
{
#ifndef ZEND_ENGINE_2
    php_sample4_autoglobal_callback("_SAMPLE4",
                              sizeof("_SAMPLE4") - 1,
                              TSRMLS_CC);
#endif
    return SUCCESS;
}

小结

通过本章的学习, 你认识了一些新的但是已经熟悉的概念, 包括内部的线程安全全局变量, 怎样向用户空间暴露诸如常量, 预初始化变量, 超级全局变量等信息. 下一章, 你将学会怎样定义和解析php.ini中的指令, 并将它们和你已经设置的内部线程安全的全局结构关联起来.


目录
上一章: php5对象
下一章: php的INI设置

你可能感兴趣的:(PHP扩展,php内核,php扩展开发,php模块开发,php源代码)