(十四)信息输出

就像我们在脚本中使用 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抛出一个编译器内部警告。通常情况下这种错误类型不应该被用户自己编写的模块所引用。


向 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 entr y");   
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<br>", 
    get_active_function_name(TSRMLS_C)
);   
zend_printf(
    "The file currently executed is %s<br>", 
    zend_get_executed_filename(TSRMLS_C)
);   
zend_printf("The current line being executed is %i<br>", 
zend_get_executed_lineno(TSRMLS_C));


(十五)启动函数与关闭函数

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


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


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


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


(十六)调用用户函数

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, 
    z_val *object,   
    zval *function_name, 
    zval **retval_ptr_ptr,   
    int param_count, 
    zval **params[],   
    int no_separation
);

需要注意的是你不必同时指定 function_table 和 object 这两个参数,只需要指定其中一个就行了。不过如果你想调用一个方法的话,那你就必须提供一个包含此方法的对象。这时 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,“Functionrequiresstringargument”); 
} 
TSRMSLS_FETCH(); 
if(call_user_function_ex(CG(function_table),NULL,*function_name,&retval,0,NULL,0)!=SUCCESS) { 
zend_error(E_ERROR,“Functioncallfailed”); } 
zend_printf(”Wehave%iastype\n”,retval->type); 
*return_value=*retval; 
zval_copy_ctor(return_value); 
zval_ptr_dtor(&retval);

调用脚本:

<?php   
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'


(十七)支持初始化文件php.ini

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,OnChangeSeco nd)   
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 "sec ond_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 *n ew_value,
    uint new_value_length, void *m h_arg1,
    void *mh_arg2, void *mh_arg3
)

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


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


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


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


(十九)关于配置文件的一些宏

由 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 应为 yes 或 no。
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> 的定义。


(二十) API 宏

下面(见表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

官方原版:http://php.net/manual/zh/internals2.ze1.zendapi.php