Entries tagged with “php manual”.


《Zend API,深入 PHP 内核》一章的初译暂时算是告一段落了。整个翻译进度前快后慢,这主要是和我本人的水平有很大关系的,同时也略微夹杂着一丝“翻译疲劳”。不过翻译的过程也是学习的过程。有很多地方都是平时囫囵吞枣,得过且过的,但这次为了翻译出去不误人子弟,被迫将所有的知识点都串联理顺了一下,自己感觉也有了很大的提高。

以后半个月或一个月我会再对初译稿进行一些审定,有些术语也尽量统一起来。整个翻译的行文现在看起来还是有点晦涩,还没有摆脱技术译稿的感觉,所以在这一点也须做些润色。在复审之后我会尝试将这些翻译提交到 PHP 手册的中文文档组,以便让更多的 PHPer 在做这方面的工作时有一点点中文参考。

同时在翻译的过程中也有了一些自己的看法,有机会再另行一些文章和大家交流。

下面(见表3.19 访问 zval 容器的 API 宏)是一些引入到 Zend API 里面用于访问 zval 容器的 API 宏。

指向
Z_LVAL(zval) (zval).value.lval
Z_DVAL(zval) (zval).value.dval
Z_STRVAL(zval) (zval).value.str.val
Z_STRLEN(zval) (zval).value.str.len
Z_ARRVAL(zval) (zval).value.ht
Z_LVAL_P(zval) (*zval).value.lval
Z_DVAL_P(zval) (*zval).value.dval
Z_STRVAL_P(zval_p) (*zval).value.str.val
Z_STRLEN_P(zval_p) (*zval).value.str.len
Z_ARRVAL_P(zval_p) (*zval).value.ht
Z_LVAL_PP(zval_pp) (**zval).value.lval
Z_DVAL_PP(zval_pp) (**zval).value.dval
Z_STRVAL_PP(zval_pp) (**zval).value.str.val
Z_STRLEN_PP(zval_pp) (**zval).value.str.len
Z_ARRVAL_PP(zval_pp) (**zval).value.ht

buildconf 处理的配置文件 config.m4 包含了所有在配置过程中所执行的指令。这些指令诸如包含测试包含所需的外部文件,像头文件、库文件等等。PHP 定义了一系列处理这类情况的宏,其中最常用的我们已经在“表3.18 config.m4 中的 M4 宏”列了出来。

表3.18 config.m4 中的 M4 宏

说明
AC_MSG_CHECKING(message) 在执行 configure 命令时输出“checking <message>”等信息。
AC_MSG_RESULT(value) 取得 AC_MSG_CHECKING 的执行结果,一般情况下 value 应为 yesno
AC_MSG_ERROR(message) 在执行 configure 命令时输出一条错误消息 message 并中止脚本的执行。
AC_DEFINE(name,value,description)

php_config.h 添加一行定义:

#define name value // description

(这对模块的条件编译很有用。)

AC_ADD_INCLUDE(path) 添加一条编译器的包含路径,比如用于模块需要为头文件添加搜索路径。
AC_ADD_LIBRARY_WITH_PATH(libraryname,librarypath) 指定一个库的连接路径。
AC_ARG_WITH(modulename,description,unconditionaltest,conditionaltest) 这是一款比较强大的宏,用于将模块的描述 description 添加到“configure –help”命令的输出里面。PHP 会检查当前执行的 configure 脚本里面有没有–with-<modulename> 这个选项。 如果有则执行 unconditionaltest 语句(比如 –with-myext=yes 等), 此时,选项的值会被包含在 $withval 变量里面。否则就执行 conditionaltest 语句。
PHP_EXTENSION(modulename, [shared]) 这个是配置你的扩展时 PHP 必定调用的一个宏。你可以在模块名后面提供第二个参数,用来表明是否将其编译为动态共享模块。这会导致在编译时为你的源码提供一个 COMPILE_DL_<modulename> 的定义。

现在你已经掌握了很多关于 PHP 的知识了。你已经知道了如何创建一个动态加载的模块或被静态连接的扩展。你还知道了在 PHP 和 Zend 的内部变量是如何储存的,以及如何创建和访问这些变量。另外你也知道了很多诸如输出信息文本、自动将变量引入符号表等一系列工具函数的应用。

尽管这一章常常有点“参考”的意味,但我们还是希望它能给你一些关于如何开始编写自己的扩展这方面的知识。限于篇幅,我们不得不省略了很多东西。我们建议你花些时间仔细研究一下它的头文件和一些模块(尤其是 ext/standard 目录下的一些文件以及 MySQL 模块,看一下这些众所周知的函数究竟是怎么实现的),看一下别人是怎么使用这些 API 函数的,尤其是那些本章没有提到的那些函数。

PHP4 重写了对初始化文件的支持。现在你可以直接在代码中指定一些初始化选项,然后在运行时读取和改变这些选项值,甚至还可以在选项值改变时接到相关通知。

如果想要为你的模块创建一个 .ini 文件的配置节,可以使用宏 PHP_INI_BEGIN() 来标识这个节的开始,并用 PHP_INI_END() 表示该配置节已经结束。然后在两者之间我们用 PHP_INI_ENTRY() 来创建具体的配置项。

PHP_INI_BEGIN()
PHP_INI_ENTRY("first_ini_entry",  "has_string_value", PHP_INI_ALL, NULL)
PHP_INI_ENTRY("second_ini_entry", "2",                PHP_INI_SYSTEM, OnChangeSecond)
PHP_INI_ENTRY("third_ini_entry",  "xyz",              PHP_INI_USER, NULL)
PHP_INI_END() 

PHP_INI_ENTRY() 总共接收 4 个参数:配置项名称、初始值、改变这些值所需的权限以及在值改变时用于接收通知的函数句柄。配置项名称和初始值必须是一个字符串,即使它们是一个整数。

更改这些值所需的权限可以划分为三种:PHP_INI_SYSTEM 只允许在 php.ini 中改变这些值;PHP_INI_USER 允许用户在运行时通过像 .htaccess 这样的附加文件来重写其值;而 PHP_INI_ALL 则允许随意更改。其实还有第四种权限:PHP_INI_PERDIR,不过我们还暂时不能确定它有什么影响。(本段关于这几种权限的说明与手册中《附录G php.ini 配置选项》一节的描述略有出入。根据译者自己查到的资料,相比之下还是《附录G php.ini 配置选项》更为准确些。译注)

第四个参数是初始值被改变时接收通知的函数句柄。一旦某个初始值被改变,那么相应的函数就会被调用。这个函数我们可以用宏 PHP_INI_MH 来定义:

PHP_INI_MH(OnChangeSecond);             // handler for ini-entry "second_ini_entry"

// specify ini-entries here
PHP_INI_MH(OnChangeSecond)
{
    zend_printf("Message caught, our ini entry has been changed to %s<br>", new_value);
    return(SUCCESS);
}

改变后的新值将会以字符串的形式并通过一个名为 new_value 变量传递给函数。要是再注意一下 PHP_INI_MH 的定义就会发现,我们实际上用到了不少参数:

#define PHP_INI_MH(name) int name(
    php_ini_entry *entry,
    char *new_value,
    uint new_value_length,
    void *mh_arg1,
    void *mh_arg2,
    void *mh_arg3
)

这些定义都可以在 php_ini.h 文件里找到。可以发现,我们的通知接收函数可以访问整个配置项、改变后的新值以及它的长度和其他三个可选参数。这几个可选参数可以通过 PHP_INI_ENTRY1(携带一个附加参数)、PHP_INI_ENTRY2(携带两个附加参数)、PHP_INI_ENTRY3(携带三个附加参数)等宏来加以指定。

关于值改变的通知函数应该被用来本地缓存一些初始花选项以便可以更快地对其访问或被用来从事一个值发生改变时所要求完成的任务。比如要是一个模块对一个主机常量进行了连接,而这时有人改变了主机名,那么就需要自动地关闭原来的连接,并尝试进行新的连接。

可以使用“表3.17 PHP 中用以访问初始化配置项的宏”来访问初始化配置项:

表3.17 PHP 中用以访问初始化配置项的宏

说明
INI_INT(name) 将配置项 name 的当前值以长整数返回。
INI_FLT(name) 将配置项 name 的当前值以双精度浮点数返回。
INI_STR(name) 将配置项 name 的当前值以字符串返回。 注意:这个字符串不是复制过的字符串,而是直接指向了内部数据。如果你需要进行进一步的访问的话,那就需要再进行复制一下。
INI_BOOL(name) 将配置项 name 的当前值以布尔值返回。(返回值被定义为 zend_bool,也就是说是一个 unsigned char)。
INI_ORIG_INT(name) 将配置项 name 的初始值以长整型数返回。
INI_ORIG_FLT(name) 将配置项 name 的初始值以双精度浮点数返回。
INI_ORIG_STR(name) 将配置项 name 的初始值以字符串返回。 注意:这个字符串不是复制过的字符串,而是直接指向了内部数据。如果你需要进行进一步的访问的话,那就需要再进行复制一下。
INI_ORIG_BOOL(name) 将配置项 name 的初始值以布尔值返回。(返回值被定义为 zend_bool,也就是说是一个 unsigned char)。

最后,你还得把整个初始化配置项引入 PHP。这项工作可以在模块的起始/结束函数中使用宏 REGISTER_INI_ENTRIES() 和 UNREGISTER_INI_ENTRIES() 来搞定。

 ZEND_MINIT_FUNCTION(mymodule)
{
    REGISTER_INI_ENTRIES();
}

ZEND_MSHUTDOWN_FUNCTION(mymodule)
{
    UNREGISTER_INI_ENTRIES();
}

PHP 还允许你在你的模块里面调用一些一些用户定义的函数,这样在实现某些回调机制(比如在做一些数组的轮循(array walking)、搜索或设计一些简单的事件驱动的程序时)时会很方便。

我们可以通过调用 call_user_function_ex() 来调用用户函数。它需要你即将访问函数表的指针、这个对象的指针(假如你访问的是类的一个方法的话),函数名、返回值、参数个数、具体的参数数组和一个是否需要进行 zval 分离的标识(这个函数原型已经“过时”了,至少是从 PHP 4.2 开始这个函数就追加了一个 HashTable *symbol_table 参数。下面所列举的函数原型更像是 call_user_function () 的声明。译注)。

ZEND_API int call_user_function_ex(
    HashTable *function_table,
    zval *object,
    zval *function_name,
    zval **retval_ptr_ptr,
    int param_count,
    zval **params[],
    int no_separation
);

需要注意的是你不必同时指定 function_tableobject 这两个参数,只需要指定其中一个就行了。不过如果你想调用一个方法的话,那你就必须提供一个包含此方法的对象。这时 call_user_function() 会自动将函数表设置为当前这个对象的函数表。而对于其他情况,只需要设定一下 function_table 而把 object 设为 NULL 就行了。

一般情况下,默认的函数表是包含有所有函数的“根”函数表。这个函数表是编译器全局变量的一部分,你可以通过 CG() 宏来访问它。如果想把编译器全局变量引入你的函数,只需先执行一下 TSRMLS_FETCH 宏就可以了。

而调用的函数名是保存在一个 zval 容器内的。猛一下你可能会感到好奇,但其实这是很合乎逻辑的。想想看,既然我们在脚本中的大部分时间都是在接收一个函数名作为参数,并且这个参数还是被转换成(或被包含在)一个 zval 容器。那还不如现在就直接把这个 zval 容器传送给函数,只是这个 zval 容器的类型必须为 IS_STRING

下一个参数是返回值 return_value 的指针。这个容器的空间函数会自动帮你申请,所以我们无需手动申请,但在事后这个容器空间的销毁释放工作得由我们自己(使用 zval_dtor())来做。

跟在 return_value 后面的是一个标识参数个数的整数和一个包含具体参数的数组。最后一个参数 no_separation 指明了函数是否禁止进行 zval 分离操作。这个参数应该总是设为 0,因为如果设为 1 的话那这个函数会节省一些空间但要是其中任何一个参数需要做 zval 分离时都会导致操作失败。

“例3.15 调用用户函数”向我们展示如何去调用一个脚本中的用户函数。这段代码调用了一个我们模块所提供的 call_userland() 函数。模块中的 call_userland() 函数会调用脚本中一个名为它的参数的用户函数,并且将这个用户函数的返回值直接作为自己的返回值返回脚本。另外你可能注意到了我们在最后调用了析构函数。这个操作或许没有太大必要(因为这些值都应该是分离过的,对它们的赋值将会很安全),但这么做总没有什么坏处,说不定在某个关键时刻它成为我们的一道“免死金牌”。:D

例3.15 调用用户函数

zval **function_name;
zval *retval;

if((ZEND_NUM_ARGS() != 1) || (zend_get_parameters_ex(1, &function_name) != SUCCESS))
{
   WRONG_PARAM_COUNT;
}
if((*function_name)->type != IS_STRING)
{
   zend_error(E_ERROR, "Function requires string argument");
}

TSRMSLS_FETCH();
if(call_user_function_ex(CG(function_table), NULL, *function_name, &retval, 0, NULL, 0) != SUCCESS)
{
   zend_error(E_ERROR, "Function call failed");
}
zend_printf("We have %i as type\n", retval->type);

*return_value = *retval;
zval_copy_ctor(return_value);
zval_ptr_dtor(&retval);

调用脚本:

dl("call_userland.so");
function test_function()
{
    echo "We are in the test function!\n";
    return 'hello';
}
$return_value = call_userland("test_function");
echo "Return value: '$return_value'";

上例将输出:

We are in the test function! We have 3 as type Return value: ‘hello’

启动函数和关闭函数会在模块的(载入时)初始化和(卸载时)反初始化时被调用,而且只调用这一次。正如我们在本章前面(见 Zend 模块描述块的说明)所提到的,它们是模块和请求启动和关闭时所发生的事件。

模块启动/关闭函数会在模块加载和卸载时被调用。请求启动/关闭函数会在每次处理一个请求时(也就是在执行一个脚本文件时)被调用。

对于动态加载的扩展而言,模块和请求的启动函数与模块和请求的关闭函数都是同时发生的(严格来说模块启动函数是先于请求启动函数被调用的,译注)。

可以用某些宏来声明和实现这些函数,详情请参阅前面的关于“Zend 模块声明”的讨论。

就像我们在脚本中使用 print() 函数一样,我们也经常需要从扩展向输出流输出一些信息。在这方面-比如输出警告信息、phpinfo() 中对应的信息等一般性任务-PHP 也为我们提供了一系列函数。这一节我们就来详细地讨论一下它们。

zend_printf()

zend_printf() 功能跟 printf() 差不多, 唯一不同的就是它是向 Zend 的输出流提供信息。

zend_error()

zend_error() 用于创建一个错误信息。这个函数接收两个参数:第一个是错误类型(见 zend_error.h),第二个是错误的提示消息。

zend_error(E_WARNING, "This function has been called with empty arguments");

“表3.16 Zend 预定义的错误信息类型” 列出了一些可能的值(在 PHP 5.0 及以上版本中又增加了一些错误类型,可参见 zend_error.h,译注)。这些值也可以用在 php.ini 里面,这样你的错误信息将会依照 php.ini 里面的设置,根据不同的错误类型而被选择性地记录。

表 3.16 Zend 预定义的错误信息类型

错误类型 说明
E_ERROR 抛出一个错误,然后立即中止脚本的执行。
E_WARNING 抛出一个一般性的警告。脚本会继续执行。
E_NOTICE 抛出一个通知,脚本会继续执行。注意: 默认情况下 php.ini 会关闭显示这种错误。
E_CORE_ERROR 抛出一个 PHP 内核错误。通常情况下这种错误类型不应该被用户自己编写的模块所引用。
E_COMPILE_ERROR 抛出一个编译器内部错误。通常情况下这种错误类型不应该被用户自己编写的模块所引用。
E_COMPILE_WARNING 抛出一个编译器内部警告。通常情况下这种错误类型不应该被用户自己编写的模块所引用。

图3.3 在浏览器中显示警告信息

在浏览器中显示警告信息

向 phpinfo() 中输出信息

在创建完一个模块之后,你可能就会想往 phpinfo() 里面添加一些关于你自己模块的一些信息了(默认是只显示你的模块名)。PHP 允许你用 ZEND_MINFO() 函数向 phpinfo() 里面添加一段你自己模块的信息。这个函数应该被放在模块描述块(见前文)部分,这样在脚本调用 phpinfo() 时模块的这个函数就会被自动调用。

如果你指定了 ZEND_MINFO 函数,phpinfo() 会自动打印一个小节,这个小节的头部就是你的模块名。其余的信息就需要你自己去指定一下格式并输出了。

一般情况下,你需要先调用一下 php_info_print_table_start(),然后再调用php_info_print_table_header()php_info_print_table_row() 这两个标准函数来打印表格具体的行列信息。这两个函数都以表格的列数(整数)和相应列的内容(字符串)作为参数。最后使用 php_info_print_table_end() 来结束打印表格。“例3.13 源代码及其在 phpinfo() 函数中的屏幕显示”向我们展示了某个样例和它的屏幕显示效果。

例3.13 源代码及其 在 phpinfo() 函数中的屏幕显示

php_info_print_table_start();
php_info_print_table_header(2, "First column", "Second column");
php_info_print_table_row(2, "Entry in first row", "Another entry");
php_info_print_table_row(2, "Just to fill", "another row here");
php_info_print_table_end();

执行时信息

你还可以输出一些执行时信息,像当前被执行的文件名、当前正在执行的函数名等等。当前正在执行的函数名可以通过 get_active_function_name() 函数来获取。这个函数没有参数(译注:原文即是如此,事实上是跟后面提到的 zend_get_executed_filename() 函数一样需要提交 TSRMLS_C 宏参数,译注),返回值为函数名的指针。当前被执行的文件名可以由 zend_get_executed_filename() 函数来获得。这个函数需要传入 TSRMLS_C 宏参数来访问执行器全局变量。这个执行器全局变量对每个被 Zend 直接调用的函数都是有效的(因为 TSRMLS_C 是我们前文讨论过的参数宏 INTERNAL_FUNCTION_PARAMETERS 的一部分)。如果你想在其他函数中也访问这个执行器全局变量,那就需要现在那个函数中调用一下宏 TSRMLS_FETCH()

最后你还可以通过 zend_get_executed_lineno() 函数来取得当前正在执行的那一行代码所在源文件中的行数。这个函数同样需要访问执行器全局变量作为其参数。关于这些函数的应用,请参阅“例3.14 输出执行时信息”。

例 3.14 输出执行时信息

zend_printf("The name of the current function is %s
", get_active_function_name(TSRMLS_C)); zend_printf("The file currently executed is %s
", zend_get_executed_filename(TSRMLS_C)); zend_printf("The current line being executed is %i
", zend_get_executed_lineno(TSRMLS_C));

输出执行时信息

关于扩展内函数到 PHP 脚本的返回值我们前面谈得比较少,这一节我们就来详细说一下。任何函数的返回值都是通过一个名为 return_value 的变量传递的。这个变量同时也是函数中的一个参数。这个参数总是包含有一个事先申请好空间的 zval 容器,因此你可以直接访问其成员并对其进行修改而无需先对 return_value 执行一下 MAKE_STD_ZVAL 宏指令。

为了能够更方便从函数中返回结果,也为了省却直接访问 zval 容器内部结构的麻烦,ZEND 提供了一大套宏命令来完成相关的这些操作。这些宏命令会自动设置好类型和数值。“表3.14 从函数直接返回值的宏”和“表3.15 设置函数返回值的宏”列出了这些宏和对应的说明。

注意:使用“表3.14 从函数直接返回值的宏”会自动携带结果从当前函数返回。而使用“表3.15 设置函数返回值的宏”则只是设置了一下函数返回值,并不会马上返回。

表3.14 从函数直接返回值的宏

说明
RETURN_RESOURCE(resource) 返回一个资源。
RETURN_BOOL(bool) 返回一个布尔值。
RETURN_NULL() 返回一个空值。
RETURN_LONG(long) 返回一个长整数。
RETURN_DOUBLE(double) 返回一个双精度浮点数。
RETURN_STRING(string, duplicate) 返回一个字符串。duplicate 表示这个字符是否使用 estrdup() 进行复制。
RETURN_STRINGL(string, length, duplicate) 返回一个定长的字符串。其余跟 RETURN_STRING 相同。这个宏速度更快而且是二进制安全的。
RETURN_EMPTY_STRING() 返回一个空字符串。
RETURN_FALSE 返回一个布尔值假。
RETURN_TRUE 返回一个布尔值真。

表3.15 设置函数返回值的宏

说明
RETVAL_RESOURCE(resource) 设定返回值为指定的一个资源。
RETVAL_BOOL(bool) 设定返回值为指定的一个布尔值。
RETVAL_NULL 设定返回值为空值
RETVAL_LONG(long) 设定返回值为指定的一个长整数。
RETVAL_DOUBLE(double) 设定返回值为指定的一个双精度浮点数。
RETVAL_STRING(string, duplicate) 设定返回值为指定的一个字符串,duplicate 含义同 RETURN_STRING
RETVAL_STRINGL(string, length, duplicate) 设定返回值为指定的一个定长的字符串。其余跟 RETVAL_STRING 相同。这个宏速度更快而且是二进制安全的。
RETVAL_EMPTY_STRING 设定返回值为空字符串。
RETVAL_FALSE 设定返回值为布尔值假。
RETVAL_TRUE 设定返回值为布尔值真。

如果需要返回的是像数组和对象这样的复杂类型的数据,那就需要先调用 array_init()object_init(),也可以使用相应的 hash 函数直接操作 return_value。由于这些类型主要是由一些杂七杂八的东西构成,所以对它们就没有了相应的宏。

迟早你会遇到把一个 zval 容器的内容赋给另外一个 zval 容器的情况。不过可别想当然,这事说起来容易做起来可有点难度。因为 zval 容器不但包含了类型信息,而且还有对 Zend 内部数据的一些引用。比如,数组以及对象等依据其大小大都或多或少包含了一些哈希表结构。而我们在将一个 zval 赋给另外一个 zval 时,通常都没有复制这些哈希表本身,复制的只是这些哈希表的引用而已。

为了能够正确复制这些复杂类型的数据,我们可以使用“拷贝构造函数(copy constructor)”来完成这项工作。拷贝构造函数在某些为了可以复制复杂类型数据而支持操作符重载的语言中有着代表性的应用。如果你在这种语言中定义了一个对象,那你就可能想为其重载(Overloading)一下“=”操作符,这个操作符通常用于将右值(操作符右边表达式的值)赋给左值(操作符左边表达式的值)。

“重载”就意味着将给予这个操作符另外一种不同的含义,它通常会把这个操作符跟某个函数调用关联起来。当这个操作符作用在一个对象上时,与之关联的函数就将会被调用,同时该操作符的左值和右值也会作为该函数的参数一并传入。这样,这个函数就可以完成“=”操作符想要完成的事情(一般是某些额外数据的复制)。

这些“额外数据的复制”对 PHP 的 zval 容器来说也是很有必要的。对于数组来说,“额外数据的复制”就是指另外再重建和复制那些与该数组有关的哈希表(因为当初我们复制 zval 时复制的仅仅是这些哈希表的指针)。而对字符串来说,“额外数据的复制”就意味着我们必须重新为字符串值去申请空间。如此类推。

Zend Engine 会调用一个名为 zval_copy_ctor()(在以前的 PHP 版本中这个函数叫做 pval_copy_constructor() )的函数来完成这项工作。

下面这个示例为我们展示了这样一个函数:它接收一个复杂类型的参数,在对其进行一定的修改后把它作为结果返回给 PHP:

zval *parameter;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z", ¶meter) == FAILURE) {
    return;
}

// 在这对参数做一定的修改
……

// 返回修改后的容器
*return_value = *parameter;
zval_copy_ctor(return_value);

函数的头一部分没什么可说的,只是一段很平常的接收参数的代码而已。不过在对这个参数进行了某些修改后就变得有趣起来了:先是把 parameter 容器值赋给了(预先定义好的)return_value 容器,然后为了能够真正复制这个容器,我们便调用了拷贝构造函数。这个拷贝构造函数能够直接处理它的参数,处理成功则返回 SUCCESS,否则返回 FAILURE

在这个例子当中如果你忘了调用这个拷贝构造函数,那么 parameterreturn_value 就会分别指向同一个 Zend 内部数据,也就是说返回值 return_value 非法指向了一个数据结构。当你修改了参数 parameter 时这个函数的返回值就可能会受到影响。因此为了创建一个独立的拷贝,我们必须调用这个函数。

在 Zend API 中还有一个与拷贝构造函数相对应的拷贝析构函数:zval_dtor(),它做的工作正好与拷贝构造函数相反。