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(),它做的工作正好与拷贝构造函数相反。

当 PHP 脚本与扩展互相交换数据时,我们还需要做一件很重要的事情,那就是创建变量。这一小节将会展示如何处理那些 PHP 脚本所支持的变量类型。

概述

要创建一个能够被 PHP 脚本所访问的“外部变量”,我们只需先创建一个 zval 容器,然后对这个 zval 结构进行必要的填充,最后再把它引入到 Zend 的内部符号表中就可以了。而且几乎所有变量的创建基本上都是这几个步骤:

zval *new_variable;

/* 申请并初始化一个新的的 zval 容器 */
MAKE_STD_ZVAL(new_variable);

/* 设置变量的类型和内容,见下 */
/* 将名为 "new_variable_name" 变量引入符号表 */
ZEND_SET_SYMBOL(EG(active_symbol_table), "new_variable_name", new_variable); 

/* 现在就可以在脚本中用 $new_variable_name 来访问这个变量了 */

宏 MAKE_STD_ZVAL 通过 ALLOC_ZVAL 来申请一个新的 zval 容器的内存空间并调用 INIT_ZVAL(查看 PHP4/5 的源代码可知此处可能为原文笔误,实际上应为 INIT_PZVAL,下同。译注)将其初始化。在当前的 Zend Engine 中,INIT_ZVAL 所负责的初始化工作除了将 zval 容器的引用计数(refcount)置为 1 之外,还会把引用标识也清除(即把 is_ref  也置为 0)。而且在以后的 Zend Engine 中还可能会继续扩展这个 INIT_ZVAL 宏操作,因此我们推荐您使用 MAKE_STD_ZVAL 而非简单使用一个 ALLOC_ZVAL 来完成一个变量的创建工作。当然,如果您是想优化一下速度(或者是不想明确地初始化这个 zval 容器),那还是可以只用 ALLOC_ZVAL 来搞定的。不过我们并不推荐这么做,因为这将不能保证数据的完整性。

ZEND_SET_SYMBOL 宏负责将我们新建的变量引入 Zend 内部的符号表。这个宏会首先检查一下这个变量是否已经存在于符号表中,如果已经存在则将其转换为一个引用变量(同时会自动销毁原有的 zval 容器)。事实上这个方法经常用在某些速度要求并不苛刻但希望能少用一些内存的情况下。

您可能注意到了 ZEND_SET_SYMBOL 是通过宏 EG 来访问 Zend 执行器(executor)的全局结构的。特别的,如果你使用的是 EG(active_symbol_table),那你就可以访问到当前的活动符号表,从而可以处理一些全局或局部变量。其中局部变量可能会依不同的函数而有所不同。

当然,要是你很在意程序的运行速度并且不在乎那一点点内存的话,那你可以跳过对相同名字变量存在性的检查而直接使用 zend_hash_update() 函数强行将这个名字的变量插入符号表。

zval *new_variable;

/* 申请并初始化一个新的的 zval 容器 */
MAKE_STD_ZVAL(new_variable);

/* 设置变量的类型和内容,见下 */

/* 将名为 "new_variable_name" 变量引入符号表 */
zend_hash_update(
    EG(active_symbol_table),
    "new_variable_name",
    strlen("new_variable_name") + 1,
    &new_variable,
    sizeof(zval *),
    NULL
);

实际上这段代码也是很多扩展使用的标准方法。

上面这段代码所产生的变量是局部变量,作用范围跟调用函数的上下文相关。如果你想创建一个全局变量那也很简单,方法还是老方法,只需换个符号表就可以了。

zval *new_variable;
/* 申请并初始化一个新的的 zval 容器 */
MAKE_STD_ZVAL(new_variable);

/* 设置变量的类型和内容,见下 */

/* 将名为 "new_variable_name" 变量引入全局符号表 */
ZEND_SET_SYMBOL(&EG(symbol_table), "new_variable_name", new_variable);

注意,现在宏 ZEND_SET_SYMBOL 使用的符号表是全局符号表 EG(symbol_table)。另外,active_symbol_table 是一个指针,而 symbol_table 却不是。这就是我们为什么分别使用 EG(active_symbol_table)&EG(symbol_table) 的原因 - ZEND_SET_SYMBOL 需要一个指针作为其参数。

当然,你同样也可以强行更新这个符号表:

zval *new_variable;

/* 申请并初始化一个新的的 zval 容器 */
MAKE_STD_ZVAL(new_variable);

/* 设置变量的类型和内容,见下 */

/* 将名为 "new_variable_name" 变量引入全局符号表 */
zend_hash_update(
    &EG(symbol_table),
    "new_variable_name",
    strlen("new_variable_name") + 1,
    &new_variable,
    sizeof(zval *),
    NULL
); 

例 3.9 “创建不同作用域的变量”向我们展示了创建一个局部变量(local_variable)和一个全局变量(global_variable)的过程。

注意:你可能会发现在 PHP 函数里似乎还不能直接访问这个全局变量(global_variable),因为你在使用前还必须使用 global $global_variable; 声明一下。

例3.9 创建不同作用域的变量

ZEND_FUNCTION(variable_creation)
{
   zval *new_var1, *new_var2;
   MAKE_STD_ZVAL(new_var1);
   MAKE_STD_ZVAL(new_var2);
   ZVAL_LONG(new_var1, 10);
   ZVAL_LONG(new_var2, 5);
   ZEND_SET_SYMBOL(EG(active_symbol_table), "local_variable", new_var1);
   ZEND_SET_SYMBOL(&EG(symbol_table), "global_variable", new_var2);
   RETURN_NULL();
}

长整型(整数)

现在让我们以长整型变量起点,了解一下如何为一个变量赋值。PHP 中的整数全部是长整型,其值的存储方法也是非常简单的。看一下我们前面讨论过的 zval.value 容器的结构你就会明白,所有的长整型数据都是直接保存在这个联合中的 lval 字段,相应的数据类型(type 字段)为 IS_LONG(见例3.10 “长整型变量的创建”)。

例3.10 长整型变量的创建

zval *new_long;
MAKE_STD_ZVAL(new_long);
new_long->type = IS_LONG;
new_long->value.lval = 10;

或者你也可以直接使用 ZVAL_LONG 宏:

zval *new_long;
MAKE_STD_ZVAL(new_long);
ZVAL_LONG(new_long, 10);

双精度型(浮点数)

PHP 中的浮点数都是双精度型,存储方法和整型差不多,也很简单。它的值是直接放在联合中的 dval 字段,对应数据类型为 IS_DOUBLE

zval *new_double;
MAKE_STD_ZVAL(new_double);
new_double->type = IS_DOUBLE;
new_double->value.dval = 3.45;

同样你也可以直接使用宏 ZVAL_DOUBLE

zval *new_double;
MAKE_STD_ZVAL(new_double);
ZVAL_DOUBLE(new_double, 3.45);

字符串

字符串的存储可能会稍费点事。字符串的值是保存在 zval.value 容器中的 str 结构里面,相应的数据类型为 IS_STRING。不过需要注意的是,前面我们已经提到过,所有与 Zend 内部数据结构相关的字符串都必须使用 Zend 自己的内存管理函数来申请空间。这样一来,就不能使用那些静态字符串(因为这种字符串的内存空间是编译器预先分配的)或通过标准函数(比如 malloc() 等函数)来申请空间的字符串。

zval *new_string;
char *string_contents = "This is a new string variable";

MAKE_STD_ZVAL(new_string);

new_string->type = IS_STRING;
new_string->value.str.len = strlen(string_contents);
new_string->value.str.val = estrdup(string_contents);

请注意,在这我们使用了 estrdup() 函数。当然我们仍可直接使用一个预定义宏 ZVAL_STRING 来完成这项工作:

zval *new_string;
char *string_contents = "This is a new string variable";

MAKE_STD_ZVAL(new_string);

ZVAL_STRING(new_string, string_contents, 1);

ZVAL_STRING 宏的第三个参数指明了该字符串是否需要被复制(使用 estrdup() 函数)。值为 1 将导致该字符串被复制,为 0 时则仅仅是简单地将其指向该变量的值容器(即字符串地址,译注)。这项特性将会在你仅仅需要创建一个变量并将其指向一个已经由 Zend 内部数据内存时变得很有用。

如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏 ZVAL_STRINGL(zval, string, length, duplicate) 来完成这项工作。这个函数会额外需要一个表明该字符串长度地参数。这个宏不但速度上要比 ZVAL_STRING 快,而且还是二进制安全的。

如果想创建一个空字符串,那么将其长度置 0 并且把 empty_string 作为字符串的内容即可:

new_string->type = IS_STRING;
new_string->value.str.len = 0;
new_string->value.str.val = empty_string;

当然,我们也专门为您准备了一个相应的宏 ZVAL_EMPTY_STRING 来搞定这个步骤:

MAKE_STD_ZVAL(new_string);
ZVAL_EMPTY_STRING(new_string);

布尔类型

布尔类型变量的创建跟长整型差不多,只是数据类型为 IS_BOOL,并且字段 lval 所允许的值只能为 0 和 1:

zval *new_bool;
MAKE_STD_ZVAL(new_bool);
new_bool->type = IS_BOOL;
new_bool->value.lval = 1;

也可以使用宏 ZVAL_BOOL (需要另外指定一个值)来完成这件事情,或者干脆直接使用ZVAL_TRUEZVAL_FALSE 直接将其值设定为 TRUEFALSE

数组

数组在 Zend 内部是用哈希表(HashTable)来存储的,这个哈希表可以使用一系列的 zend_hash_*() 函数来访问。因此我们在创建一个数组时必须先创建一个哈希表,然后再将其保存在 zval.value 容器的 ht 字段中。

不过针对数组的创建我们现在另有一套非常方便 API 可供使用。为了创建一个数组,我们可先调用一下 array_init() 函数:

zval *new_array;
MAKE_STD_ZVAL(new_array);
array_init(new_array); // array_init() 函数总是返回 SUCCESS

要给数组增加一个元素,根据实际需要,我们有 N 个函数可供调用。“表3.8 用于关联数组的 API”、“表3.9 用于索引数组的 API 第一部分”和“表3.10 用于索引数组的 API 第二部分”有这些函数的说明。所有这些函数在调用成功时返回 SUCCESS,在调用失败时返回 FAILURE。

表3.8 用于关联数组的 API

函数 说明
add_assoc_long(zval *array, char *key, long n); 添加一个长整型元素。
add_assoc_unset(zval *array, char *key); 添加一个 unset 元素。
add_assoc_bool(zval *array, char *key, int b); 添加一个布尔值。
add_assoc_resource(zval *array, char *key, int r); 添加一个资源。
add_assoc_double(zval *array, char *key, double d); 添加一个浮点值。
add_assoc_string(zval *array, char *key, char *str, int duplicate); 添加一个字符串。duplicate 用于表明这个字符串是否要被复制到 Zend 的内部内存。
add_assoc_stringl(zval *array, char *key, char *str, uint length, int duplicate); 添加一个指定长度的字符串。其余跟add_assoc_string () 相同。
add_assoc_zval(zval *array, char *key, zval *value); 添加一个 zval 结构。 这在添加另外一个数组、对象或流等数据时会很有用。

表3.9 用于索引数组的 API 第一部分

函数 说明
add_index_long(zval *array, uint idx, long n); 添加一个长整型元素。
add_index_unset(zval *array, uint idx); 添加一个 unset 元素。
add_index_bool(zval *array, uint idx, int b); 添加一个布尔值。
add_index_resource(zval *array, uint idx, int r); 添加一个资源。
add_index_double(zval *array, uint idx, double d); 添加一个浮点值。
add_index_string(zval *array, uint idx, char *str, int duplicate); 添加一个字符串。duplicate 用于表明这个字符串是否要被复制到 Zend 的内部内存。
add_index_stringl(zval *array, uint idx, char *str, uint length, int duplicate); 添加一个指定长度的字符串。其余跟add_index_string () 相同。
add_index_zval(zval *array, uint idx, zval *value); 添加一个 zval 结构。 这在添加另外一个数组、对象或流等数据时会很有用。

表3.10 用于索引数组的 API 第二部分

函数 说明
add_next_index_long(zval *array, long n); 添加一个长整型元素。
add_next_index_unset(zval *array); 添加一个 unset 元素。
add_next_index_bool(zval *array, int b); 添加一个布尔值。
add_next_index_resource(zval *array, int r); 添加一个资源。
add_next_index_double(zval *array, double d); 添加一个浮点值。
add_next_index_string(zval *array, char *str, int duplicate); 添加一个字符串。duplicate 用于表明这个字符串是否要被复制到 Zend 的内部内存。
add_next_index_stringl(zval *array, char *str, uint length, int duplicate); 添加一个指定长度的字符串。其余跟add_next_index_string () 相同。
add_next_index_zval(zval *array, zval *value); 添加一个 zval 结构。 这在添加另外一个数组、对象或流等数据时会很有用。

所有这些函数都是对 Zend 内部 hash API 的一种友好抽象。因此,若你愿意,你大可直接使用那些 hash API 进行操作。比方说,假如你已经有了一个 zval 容器并想把它插入到一个数组,那么你就可以直接使用 zend_hash_update() 来把它添加到一个关联数组(例3.11 给关联数组添加一个元素)或索引数组(例3.12 给索引数组添加一个元素)。

例3.11 给关联数组添加一个元素

zval *new_array, *new_element;
char *key = "element_key";
MAKE_STD_ZVAL(new_array);
MAKE_STD_ZVAL(new_element);
array_init(new_array);
ZVAL_LONG(new_element, 10);
if(zend_hash_update(new_array->value.ht, key, strlen(key) + 1, (void *)&new_element, sizeof(zval *), NULL) == FAILURE)
{
    // do error handling here
} 

例3.12 给索引数组添加一个元素

zval *new_array, *new_element;
int key = 2;
MAKE_STD_ZVAL(new_array);
MAKE_STD_ZVAL(new_element);
array_init(new_array);
ZVAL_LONG(new_element, 10);
if(zend_hash_index_update(new_array->value.ht, key, (void *)&new_element, sizeof(zval *), NULL) == FAILURE)
{
    // do error handling here
} 

如果还想模拟下 add_next_index_*() ,那可以这么做:

zend_hash_next_index_insert(ht, zval **new_element, sizeof(zval *), NULL)

注意:如果要从函数里面返回一个数组,那就必须首先对预定义变量 return_value (return_value 是我们导出函数中的一个预定义参数,用来存储返回值)使用一下 array_init() 函数。不过倒不必对其使用 MAKE_STD_ZVAL 。

提示:为了避免一遍又一遍地书写 new_array->value.ht,我们可以用 HASH_OF(new_array) 来代替。而且出于兼容性和风格上的考虑,我们也推荐您这么做。

对象

既然对象可以被转换成数组(反之亦然),那么你可能已经猜到了两者应该具有很多相似之处。实际上,对象就是使用类似的函数进行操作的,所不同的是创建它们时所用的 API。

我们可以调用 object_init() 函数来初始化一个对象:

zval *new_object;
MAKE_STD_ZVAL(new_object);
if(object_init(new_object) != SUCCESS)
{
    // do error handling here
}

可以使用“表3.11 用于创建对象的 API”来给对象添加一些成员。

表3.11 用于创建对象的 API

函数 说明
add_property_long(zval *object, char *key, long l); 添加一个长整型类型的属性值。
add_property_unset(zval *object, char *key); 添加一个 unset 类型的属性值。
add_property_bool(zval *object, char *key, int b); 添加一个布尔类型的属性值。
add_property_resource(zval *object, char *key, long r); 添加一个资源类型的属性值。
add_property_double(zval *object, char *key, double d); 添加一个浮点类型的属性值。
add_property_string(zval *object, char *key, char *str, int duplicate); 添加一个字符串类型的属性值。
add_property_stringl(zval *object, char *key, char *str, uint length, int duplicate); 添加一个指定长度的字符串类型的属性值,速度要比 add_property_string() 函数快,而且是二进制安全的。
add_property_zval(zval *obect, char *key, zval *container); 添加一个 zval 结构的属性值。 这在添加另外一个数组、对象等数据时会很有用。

资源

资源是 PHP 中一种比较特殊的数据类型。“资源”这个词其实并不特指某些特殊类型的数据,事实上,它指的是一种可以维护任何类型数据信息方法的抽象。所有的资源均保存在一个 Zend 内部的资源列表当中。列表中的每份资源都有一个指向可以表明其种类的类型定义的指针。Zend 在内部统一管理所有对资源的引用。直接访问一个资源是不大可能的,你只能通过提供的 API 来对其进行操作。某个资源一旦失去引用,那就会触发调用相应的析构函数。

举例来说,数据库连接和文件描述符就是一种资源。MySQL 模块中就有其“标准”实现。当然其他模块(比如 Oracle 模块)也都用到了资源。

注意:

实际上,一个资源可以指向函数中任何一种你所感兴趣的数据(比如指向一个结构等等)。并且用户也只能通过某个资源变量来将资源信息传递给相应的函数。

要想创建一个资源你必须先注册一个这个资源的析构函数。这是因为Zend 需要了解当你把某些数据存到一个资源里后,如果不再需要这份资源时该如何将其释放。这个析构函数会在释放资源(无论是手工释放还是自动释放)时被 Zend 依次调用。析构函数注册后,Zend 会返回一个此种资源类型句柄。这个句柄会在以后任何访问此种类型的资源的时候被用到,而且这个句柄绝大部分时间都保存在扩展的全局变量里面。这里你不需要担心线程安全方面的问题,因为你只是需要在模块初始化注册一次就行了。

下面是这个用于注册资源析构函数的 Zend 函数定义:

ZEND_API int zend_register_list_destructors_ex(rsrc_dtor_func_t ld, rsrc_dtor_func_t pld, char *type_name, int module_number);

你或许已经注意到了,在该函数中我们需要提供两种不同的资源析构函数:一种是普通资源的析构函数句柄,一种是持久化资源的析构函数句柄。持久化资源一般用于诸如数据库连接等这类情况。在注册资源时,这两个析构函数至少得提供一个,另外一个析构函数可简单地设为 NULL。

zend_register_list_destructors_ex() 接受以下几个参数:

ld 普通资源的析构函数。
ld 持久化资源的析构函数。
type_name 为你的资源指定一个名称。在 PHP 内部为某个资源类型起个名字这是个好习惯(当然名字不能重复)。用户调用 var_dump($resource) 时就可取得该资源的名称。
module_number 这个参数在你模块的 PHP_MINIT_FUNCTION 函数中会自动定义,因此你大可将其忽略。

返回值是表示该资源类型的具有唯一性的整数标识符,即资源类型句柄

资源(不论是不是持久化资源)的析构函数都必须具有以下的函数原型:

void resource_destruction_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC);

参数 rsrc 指向一个 zend_rsrc_list_entry 结构:

typedef struct _zend_rsrc_list_entry {
    void *ptr;
    int type;
    int refcount;
} zend_rsrc_list_entry;

成员 void *ptr 才真正指向你的资源。

现在我们就知道该怎么开始了。我们先定义一个将要注册到 Zend 内部的资源类型 my_resource,这个类型的结构很简单,只有两个整数成员:

typedef struct {
    int resource_link;
    int resource_type;
} my_resource;  

接着我们再定义一下这种资源的析构函数。这个析构函数大致上是以下这个样子:

void my_destruction_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC) {
    // 先将无类型指针转换为我们的资源类型指针
    my_resource *my_rsrc = (my_resource *) rsrc->ptr;

    // 现在我们就可以随意处理这些资源了:像关闭文件、释放内存等等。
    // 当然也不要忘了释放资源本身所占用的内存! 
    do_whatever_needs_to_be_done_with_the_resource(my_rsrc);
}

注意:

有一个很重要的事情必须要提一下:如果你的资源是一个比较复杂的结构,比如包含有你在运行时所申请内存的指针等,那你就必须在释放资源本身前释放它们!

OK。现在我们定义了

  1. 我们的资源是什么样子;
  2. 我们资源的析构函数是什么样子。

那么,我们还需要做哪些工作呢?我们还需要:

  1. 创建一个在整个扩展范围内有效的全局变量用于保存资源类型句柄,这样就可以在每个需要它的函数中都能访问到它;
  2. 给我们的资源类型定义一个名称;
  3. 完成前面定义的资源析构函数;
  4. 最后注册这个析构函数。
    // 在你扩展的某个地方定义一个表示资源类型的变量
    static int le_myresource;

    // 给我们的资源起个名字是个很不错的习惯
    #define le_myresource_name  "My type of resource" 

   [...]

    // 现在完成我们资源的析构函数
    void my_destruction_handler(zend_rsrc_list_entry *rsrc TSRMLS_DC) {
       my_resource *my_rsrc = (my_resource *) rsrc->ptr;
       do_whatever_needs_to_be_done_with_the_resource(my_rsrc);
    }

    [...] 
    PHP_MINIT_FUNCTION(my_extension) {
        // 注意 'module_number' 已经在 PHP_MINIT_FUNCTION() 函数中被定义过了
        le_myresource = zend_register_list_destructors_ex(my_destruction_handler, NULL, le_myresource_name, module_number);
        
        // 然后你可以在这里注册一些附加资源、初始化全局变量、常量等等。
   }  

注册完这种资源的析构函数后,要真正注册一个资源(实例),我们可以使用 zend_register_resource() 函数或使用 ZEND_REGISTER_RESOURE() 宏。这两个的定义可以在 zend_list.h 中找到。尽管两者的参数定义都是一一对应的,但使用宏通常可以得到更好的前向兼容性:

int ZEND_REGISTER_RESOURCE(zval *rsrc_result, void *rsrc_pointer, int rsrc_type);  
rsrc_result 这是一个初始化过 zval * 容器。
rsrc_pointer 指向所保存的资源。
rsrc_type 这个参数就是你在注册函数析构函数时返回的资源类型句柄。对上面的代码来说就是le_myresource le_myresource。

返回值就是表示这个资源(实例)的具有唯一性的整数。

那么在我们注册这个资源(实例)时究竟发生了什么事呢?函数会从 Zend 内部某个列表取得一个空闲空间,然后将资源指针及类型保存到这个空间。最后这个空闲空间的索引被简单地保存在给定的 zval * 容器里面:

rsrc_id = zend_list_insert(rsrc_pointer, rsrc_type);
if (rsrc_result) {
    rsrc_result->value.lval = rsrc_id;
    rsrc_result->type = IS_RESOURCE;
}
return rsrc_id;  

返回值 rsrc_id 就唯一性地标识了我们新注册得到的那个资源。你可以使用宏RETURN_RESOURE 来将其返回给用户:

RETURN_RESOURCE(rsrc_id)

注意:

如果你想立刻把这个资源返回给用户,那你就应该把 return_value 作为那个 zval * 容器。这也是我们推荐的一种编程实践。

Zend 引擎从现在就会开始跟踪所有对这个资源的引用。一旦对这个资源的引用全都不存在了,那么你在前面为这个资源所注册的析构函数就会被调用。这样做的好处就是你不用担心会在你的模块里面引入内存泄漏-你只需要把你调用脚本中所有需要分配的内存都注册成资源即可。这样一来,一旦脚本认为不再需要它们的时候,Zend 就会找到它们然后再通知你(这就是 callback,译注)。

现在用户已经通过在某处传入到你函数的参数拿到了他的资源。zval * 容器中的 value.lval 包含了你资源的标识符,然后他就可以宏 ZEND_FETCH_RESOURCE 来获取资源了:

ZEND_FETCH_RESOURCE(rsrc, rsrc_type, rsrc_id, default_rsrc_id, resource_type_name, resource_type)

rsrc 这个指针将指向你前面已经声明过的资源。
rsrc_type 这个参数用以表明你你想要把前面参数的那个指针转换为何种类型。比如 myresource * 等等。
rsrc_id 这个是用户传进你函数的那个 zval *container 的地址。 假如给出的是 zval *z_resource ,那么此处就应该是 &z_resource
default_rsrc_id 这个参数表明假如没有取到资源时默认指定的资源标识符。通常为 -1。
resource_type_name 所请求的资源类型资源类型名称。当不能找到资源时,就用这个字符串去填充系统由于维护而抛出的错误信息。
resource_type 这个可以取回在注册资源析构函数时返回的资源类型。在本例就是 le_myresource

这个宏没有返回值。这对开发人员可能会方便了点。不过还是要注意添加 TSRM 参数和确认一下是否取回了资源。如果在接收资源时出现了问题,那它就会抛出一个警告信息并且会立刻从当前函数返回,其返回值为 NULL。

如果想从列表强行删除一个资源,可以使用 zend_list_delete() 函数。当然也可以强行增加引用计数,如果你知道你正在创建一个指向已分配内存资源的引用(比如说你可能想重用一个默认的数据库连接)。对于这种情况你可以使用函数 zend_list_addref() 。想要查找一个已分配内存的资源,请使用 zend_list_find() 函数。关于这些操作的完整 API 请参见 zend_list.h

自动创建全局变量的宏

作为我们早期所谈论的一些宏的补充,还有一些宏可以让我们很方便的创建全局变量。了解了它们,我们在引入一些全局标识时就会感觉很爽,不过这个习惯可能会不太好。在“表3.12 创建全局变量的宏”中描述了完成这些任务所用到的正确的宏。它们不需要申请任何 zval 容器,你只需简单地提供一个变量名和其值即可。

表3.12 创建全局变量的宏

说明
SET_VAR_STRING(name, value) 新建一个字符串变量。
SET_VAR_STRINGL(name, value, length) 新建一个指定长度的字符串变量。这个宏要比 SET_VAR_STRING 快而且还是二进制安全的。
SET_VAR_LONG(name, value) 新建一个长整型变量。
SET_VAR_DOUBLE(name, value) 新建一个双精度变量。

创建常量

Zend 支持创建真正的常量。访问常量时不需要 $ 前缀,而且常量是全局有效的。比如 TRUEFALSE 这两个常量。

要想创建一个常量,你可以使用“表3.13 创建常量的宏”中所列举的宏来完成这项工作。所有的宏在创建常量时都必须指定一个名称和值。

你还可以为常量指定一个特别的标识:

  • CONST_CS – 这个常量的名称是大小写敏感的;
  • CONST_PERSISTENT – 这个常量是持久化的。换句话说,当携带这个常量的进程关闭时这个常量在剩下的请求中还依然有效,并不会被“遗忘”。

可以使用二进制的“或(OR)”操作来使用其中的一个或两个标识:

// 注册一个长整型常量
REGISTER_LONG_CONSTANT("NEW_MEANINGFUL_CONSTANT", 324, CONST_CS | CONST_PERSISTENT);

我们提供有两种不同类型的宏,分别是 REGISTER_*_CONSTANTREGISTER_MAIN_*_CONSTANT。第一种类型在创建常量时只会绑定到当前模块。一旦注册这个模块的常量从内存中卸载,那么这个常量也就会随即消逝。第二种类型创建的变量将会独立于该模块,始终保存在符号表中。

表3.13 创建常量的宏

说明
REGISTER_LONG_CONSTANT(name, value, flags) REGISTER_MAIN_LONG_CONSTANT(name, value, flags) 新建一个长整型常量。
REGISTER_DOUBLE_CONSTANT(name, value, flags) REGISTER_MAIN_DOUBLE_CONSTANT(name, value, flags) 新建一个双精度型常量。
REGISTER_STRING_CONSTANT(name, value, flags) REGISTER_MAIN_STRING_CONSTANT(name, value, flags) 新建一个字符串常量。给定的字符串的空间必须在Zend 内部内存。
REGISTER_STRINGL_CONSTANT(name, value, length, flags) REGISTER_MAIN_STRINGL_CONSTANT(name, value, length, flags) 新建一个指定长度的字符串常量。同样,这个给定的字符串的空间也必须在Zend 内部内存。

算起来这个站点都快有三个月打不开了。不过还好,在一些朋友的帮助下现在似乎是没什么大问题了。

尽管在这个 Blog 上有三个月的空白,但在实际生活中却并非如此。相反,这三个月经过的事情貌似比上一年加起来还多。当然,过去的已经过去,新的生活才刚刚开始~

本年度剩余时间的打算:

  1. 完善一下现有的 PHP 反编译器,完成难度 4;
  2. 写一款较为完善的 PHP 加密工具,完成难度 5;
  3. 使用 Delphi 写一款 PHP 的 IDE,完成难度 7;
  4. 找一份合适的工作,完成难度 8;
  5. 找一个合适的MM,完成难度 10。:(

临近年关,可能大家都忙了些,因此虽然我们已经提前做了准备,并且遵守每双月 1 号发布一期杂志的承诺,按时发布了新一期的杂志,但在编辑部内部还是感觉匆忙了些,希望下一期能有所改观。

这一期的杂志并没有我的文章,我也不再负责每期《PHP 新闻》和《编者按》,而纯粹是作为一个《安全与优化》栏目的编辑出现,现在看来自我感觉做得还不够好。坦白说,这个栏目还没有做到我心目中所要达到的高度,今后还要多多努力。

相对于第一期,这一期杂志厚度略显薄了一些。不过单就页码数(本期为 69 页)而言,我认为比较合适的。太少自然说不过去,太多则编辑压力太大,而且限于目前国内 PHP 的应用现状也会有巧妇难做无米之炊的感觉。

关于本期的文章,限于个人兴趣,我主要说说这方面的几篇文章。

先来说说我负责栏目的几篇文章。得益于近水楼台,我最先翻阅的是 heiyeluren(黑夜路人)的《分表处理设计思想和实现》这篇文章。看得出,heiyeluren 应该是一名实际经验非常丰富的 PHP 程序员了。不过我并不想再次赘述文章的内容,而是想说一个看他的几篇文章所得出来的一个非常有趣的看法:他在文末所提出问题的价值通常都高于文章本身的平均价值。:D 我甚至觉得那几个问题才是文章的点睛之笔。

除这篇文章外,还有两篇关于安全方面的文章,分别是残恤的《编写安全的 PHP 代码》和剑心的《变量没有初始化引起安全漏洞》。这两篇文章作为我个人来讲,是有点言犹未尽的感觉的。在和作者交流时,作者似乎也有类似的感觉。这可能是我催稿也有点晚,使两位作者未有充分的时间去酝酿,来不及把自己的想法完全舒展开来的缘故,下次一定要记得改进。

存储过程、触发器和视图是构建大中型应用中不可避免地所使用的技术手段,这方面特性的缺乏也是 MySQL 被经常诟病的地方之一。所幸 MySQL 已经意识到了这个问题,在 5.x 版本中已经加入了对这方面的支持。但令人遗憾的是介绍 MySQL 存储过程等新特性的文章并不多,尤其是中文文章。本期 welefen 的《MySQL 中的存储过程、触发器和视图》一篇文章相信会对很多朋友有所帮助。

h058 的《JavaScript 效率测试》一文我也仔细的看了一遍。由于我对 JavaScript 的内部结构不太熟悉,所以也是感觉收获颇丰。

另外还有李辉的《Smarty 结合 ajax 无刷新留言本实例解析》、周路明的《PHPChina 留言本实例》系列连载教程都是很不错的技术文章,是刚进 PHP 大门朋友的一份很好的参考资料。

其他都是些非技术性文章,就不多说了,感兴趣的可以去看看,放松一下。:)

这一版本的主要新增功能包括:

– 用于官方的安全修正和勘误二进制更新的 freebsd-update(8)
– 对 BSM 安全事件审计的试验性支持
– OpenBSM 命令行审计工具和函数库
– KDE 3.5.4、GNOME 2.16.1
– 集成的 cvsup 客户端 csup(1)
– 为 geli(4) 加入了磁盘完整性检验保护
– 新增了 amdsmb(4), enc(4) ipmi(4), nfsmb(4), stge(4) 驱动
– IPFW(4) 数据包 tag 功能
– Linux 模拟层的 sysfs 支持
– BIND 9.3.3
– 包括 em(4), arcmsr(4), ath(4), bce(4), ata(4), and iwi(4) 在内的一系列驱动更新

以及性能和可靠性方面的改进,另外还增加了一个关于 doc 的 ISO 文件。这一版本目前已经可以从FreeBSD的许多镜像站点下载了。

经过 N 次的跳票和等待,PHP 5.2.0 已于 11 月 2 日正式发布。根据 PHP 小组不成文的开发习惯,PHP 5.0 系列更大程度上像一个技术展示版,性能低下(甚至还不如 PHP4,这主要归结与 Zend 中重写的更加强大也更加复杂的OO 机制),稳定性也有所欠缺,目前 PHP 开发组已经放弃了对这个系列的开发。PHP 5.1 系列则侧重于对性能的改善,和 PHP 5.0 系列已经不可同日而语。但随着 PHP5.2系列的发布,PHP 5.1 也已经和 PHP4.4 系列一样进入了维护状态,除非出现重大的 BUG 和安全隐患否则将不再更新。新发布的 PHP 5.2 除了修复了以往 200 多个 BUG 以外,它还将性能进一步提高,尤其是改善了在高负载情况下的表现,而且在安全性的处理上也做出了很大的改进。同时也增加了很多很实用的技术(比如 JSON、Zip等支持),另外还有一些原本定为 PHP6 的特性也已经被提前实现在这个版本当中。可以说,相对于最初的 PHP5 版本,这次的改变几乎是半革命性的(革命性这个形容词当然是留给 PHP6 的 :D)。下面我自己掌握的一些信息和理解,尝试向大家介绍一下 PHP 5.2的新特性,如有不当之处,还望方家斧正:

1、最明显的当属 PHP 5.2 在移除了filepro 和 hwapi 两个扩展(这两个被移动到了 PECL)的同时另外增加了4个新的扩展:Date、JOSN、Filter和ZIP。

其实从 PHP 5.1 开始,在 PHP 核心增加了一个 Date 扩展,重写了对日期/时间(主要是时区方面)的支持。所不同的是,PHP 5.2 更进一步,日期和时区则分别成为了类 DateTime 和 DateTimeZone 的一个对象。大家可以在 PHP CLI 中运行php –rc DateTime 或 php –rc DateTimeZone 来看一下这两个类的详细信息。需要注意的是如果你的程序中已经存在有名为 DateTime 或 DateTimeZone 类的话,那想在 PHP 5.2 中运行就必须改名了。(著名的开源 CRM :vtigercrm 已经有了前车之鉴)

JSON 扩展实现了则实现对 JavaScript Object Notation (JSON) 这种轻量级数据交换格式的支持。在 PHP 5.2 中这个扩展是默认被启用的。

相信很多开发人员都为 PHP与 JavaScript 之间的通信发愁过,尤其是现在这个流行 Ajax 的时代。而现在,我们用JSON 就可以很轻易的解决这个问题。JSON 数据能够直接为服务器端代码使用, 并且也能够让客户端的 JavaScript简单地通过 eval() 来进行读取,这就大大简化了服务器端和客户端的代码开发量。虽然以前在 PHP 里面也有一些 JSON 类来支持,但这哪有 PHP 的原生支持来得高效和快捷呢?Filter 扩展负责校验和过滤数据,这个扩展主要是为了处理像用户的输入那样的不可靠数据而设计的。这个扩展也是默认被启用的。默认情况下的RAW 模式将不会以任何方式影响输入的数据,也就是说这不会对现有代码产生任何影响。但我们在以后的开发中应该尽可能地利用这个扩展来进行敏感字符的过滤,因为这不但简化了一些表单的验证工作,而且提高了程序的安全性和运行效率。 ZIP 扩展将允许我们对 Zip 压缩包及其包内的文件进行读写操作。也就是说,我们对 Zip文件不仅能看,而且还能摸:它提供了对 Zip 文件的完全支持。这项特性的应用是非常广泛的,具体带来的种种好处我就不再多说了。

当然,关于这些扩展的具体细节还得查阅我们无所不能的 PHP 手册。:D

2、改进了内存管理器,使之在高负载情况下具有更佳的表现。

按照 “Zend 二老”之一也就是 “Zend” 中那个 “nd” 的说法,这个新的内存管理器是分层(hierarchical)的。这个管理器共有三层:存储层(storage)、堆(heap)层和 emalloc/efree 层。存储层通过 malloc()、mmap() 等函数向系统真正的申请内存,并通过 free() 函数释放所申请的内存。存储层相当于 Zend 自己的“内存仓库”,通常申请的内存块都比较大。堆层(注意这里的堆并不是指操作系统所管理的堆,而是 Zend 内存管理器的所管理的内存堆)就把它们从存储层要过来并分割成一些较小的块。而 emalloc/efree 层就是指通过 PHP API 的 emalloc()/efree() 函数所申请和释放的内存。emalloc() 并不直接同存储层打交道,同它们接头的是堆层。负责把比原来的管理器在同等条件下会分配更小的内存,但速度更快。它首先会从系统(堆)中申请一些较大的内存块,然后自己来管理这个堆。php.ini 中的memory_limit 值虽然还会被检用,但已不再是每次 emalloc() 调用时都检用,而仅仅是在向系统申请那些大的内存块时被检用。

熟悉一些服务器端编程的朋友可能会马上想到一个词:内存池!没错,这基本上就是一个内存池。至少我是这么认为的。:D

内存池技术其实这是服务端编程中很常见。使用内存池技术可以有效地避免频繁的内存申请/释放操作,在内存池技术中,内存释放时实际上并没有通知操作系统真实的释放和回收,而仅仅是对将要释放的内存做个了标记,表示该部分内存已经不再使用。等下一次申请内存时,就从这些“可用”的内存链表中取出一个内存块,从而避免了频繁的内存申请/释放操作,大大节省了系统资源。根据测试和统计,在 PHP 4.4 版本中,一个典型的较为简单的请求就有超过 20,000次的对系统堆的申请和释放操作,这花费的时间相当于整个脚本所花费时间的 20% 左右。由此可见,若能降低这种资源消耗则效果是极为可观的。

与此同时,采用内存池技术也带来一个更为重要的“副作用”:避免了大量的“内存碎片”。一般情况下,内存碎片是对系统没有多大影响的,但服务端应用明显不同于一般应用,服务端会面临很高数量级的访问请求,这些请求也伴随着更高数量级的内存申请/释放操作。内存碎片过多将导致内存利用率降低,降低内存分配速度,严重时还会触发内存分配失败,尽管此时理论上仍有可分配的物理内存。

最后,内存池技术还有一个不太引人注意的好处:它能降低内存泄漏的概率。因为实际申请的内存块虽然尺寸较大,但数量较少,而且一般情况下都是统一申请,实际释放时也可以统一释放。显然,这比代码中即时申请,而没有即时释放所造成内存泄漏的概率要小很多。

不过采用内存池技术的管理器也明显要比常规管理器(指的是内存即时申请,即时释放的管理机制)的内存开销要大,因为除了真正申请的内存外,管理器还得负责维护每个内存块的状态,因此 PHP 5.2 中把 php.ini中 memory_limit 指令值从默认的 8M 提升为 16M 。这看似增加了内存消耗,并且也需要极少量的CPU资源来管理,但由于内存碎片的减少实际上并没有增加多少消耗,再结合其他方面的表现,可以说是所得远大于所失,尤其是在高负载情况下。

3、PHP 5.2 也对 INI 指令的存取方面做了优化。

PHPer 都知道,在 PHP 脚本中我们可以用 ini_set() 函数动态改变某个 PHP 指令的值。但问题是在请求结束后你还得把这些改变的值给恢复过来以保证下个脚本能正确使用。在 PHP 5.2 之前,PHP 的做法可谓是不辞劳苦,逐个遍历 INI 指令并恢复的。如果你是整个脚本都是 ini_set() 也就罢了(当然这个情况也是极其罕见的。:)),万一我的脚本中很少使用甚至根本就没有使用这个函数那我不就是亏大了?因此 PHP 5.2 为了解决这个问题又额外增加了一个表专门保存更改过的指令,这样就不用来回挨个恢复了。

4、PHP 5.2 还对 require_once() 和 include_once() 两个函数进行了优化。

PHP 5.2 以前 require_once() 和 include_once() 的做法是无论某个文件是否已经被缓存或编译过,统统是先 fopen() 再说,打开成功后在查询一下是否已经缓存过。这么处理的原因就是 在 PHP 5.1 以前没有很完美的解决 realpath() 相对路径和符号连接方面的问题。因为若不能唯一地正确地确定某个路径的真实路径表示那么你就无法利用这个路径的唯一性去解决某个问题。而 fopen 则没有这个顾虑。realpath() 的这个问题在 PHP 5.1 中被彻底搞定了,但还没来得及应用到 require_once() 和 include_once() ,结果就拖迟到现在。解决这个问题的好处是在于避免了 fopen 这个 I/O 操作,在很多高负载情形中,通常都是 数据库、网络或者磁盘 I/O 而不是 CPU 成为瓶颈。

5、对HashTable 的复制也进行了优化。

HashTable在ZendEngine是一个很基本的数据结构,数组本质上就是一个 HashTable。对HashTable 的优化也将意味着对数组的复制操作(无论是显式还是隐式)速度将会有一定的提升。

6、其他的一些性能方面的改进。

PHP 5.2 也对在FastCGI SAPI 模块中访问环境变量的性能做了少许优化。以前则是逐行搜索,现在则是通过 Hash 值来存取。除此之外,PHP 5.2还对str_replace() 和 implode() 函数以及“try {} catch {}”块等都做了一定的优化。

还有一些对语言特性和安全特性方面的改进:

1、继PHP 5.0增加了一个 E_STRICT的错误报告级别(常量值为 2048)之后,PHP 5.2 也新增了一个错误报告

级别:E_RECOVERABLE_ERROR ,其常量值为 4096 。

这个级别的错误主要是从E_ERROR中但可以被用户自定义的错误处理程序(一般通过set_error_handler() 函数指定)所捕捉的情况转化而来。如果一个E_RECOVERABLE_ERROR 未被捕捉并处理,那么它的表现就和所有 PHP 版本中的E_ERROR一样会导致程序中止。在错误日志中,该类型的错误将被记录成“可捕捉的致命错误(Catchable fatal error)”。

导致 PHP 抛出 E_RECOVERABLE_ERROR 的情况通常是指那些很危险,但还不足以让 Zend Engine 崩溃的情况。比方说,有下面一段代码:

class foo {
function bar(foo $a) { }
}
$a = new foo ;
$a->bar(new stdClass) ;

很明显,类 foo 的 bar 函数要求一个 foo 类型的参数,但实际代码中却给了一个 stdClass 类型的参数。在PHP 5.2 以前,这会导致一个 E_ERROR(Fatal error: Argument 1 passed to foo::bar() must be an instance of foo……)。但在 PHP 5.2(包括以后的 PHP6)则会导致一个E_RECOVERABLE_ERROR(Catchable fatal error: Argument 1 passed to foo::bar() must be an instance of foo……)。这种错误是可以被捕捉的,如果你通过set_error_handler() 指定了一个错误处理函数(即使是你在这个函数中没有处理E_RECOVERABLE_ERROR),那么程序就会继续运行。但如果你没有指定一个错误处理函数,那么这个 E_RECOVERABLE_ERROR 错误就会和 E_ERROR 一样,会立即导致程序中止。

2、相应的,错误报告级别 E_ALL 也将会包含上述E_RECOVERABLE_ERROR。

这也就意味着常量 E_ALL 的值将会从原来的2047 变为 6143。注意,在 PHP 5.0 和 PHP 5.1 中虽然增加了E_STRICT,但在这两个版本中 E_ALL 并不包含 E_STRICT。而在以后的版本(如 PHP5.2、PHP6等)中 E_ALL 则包含了包括 E_STRICT 和 E_RECOVERABLE_ERROR 在内的所有错误级别。 在 PHP 5.0/5.1 中我们想设置error_reporting 为 E_ALL 就不得不采用error_reporting(E_ALL | E_STRICT) 的写法,感觉极为别扭,也很容易造成一些疏忽和误导。在 PHP5.2当中我们就没有这个苦恼了。另外如果你在 Apache 的配置文件(如 httpd.conf)或 .htaccess 文件中用error_reporting 设置了错误报告级别(比如:php_value error_reporting 4095),由于 Apache 不支持 PHP 常量 ,那你还得手工去适当调整这些错误报告级别的数值。

3、添加了allow_url_include 这个 ini 指令来辅助 allow_url_fopen 操作;

这个是 PHP 5.2 在安全方面的重大更新之一。使用这个指令可以让我们区分开对远程文件的标准文件操作和包含操作。我们通常需要进行前面的标准操作,而后面的包含操作则通常是危险的发源地。从 PHP 5.2 开始,你的本地脚本可以在禁止远程包含操作的同时进行标准远程文件操作。事实上,这个就是默认配置。 PHP 5.2 把原来的allow_url_fopen 指令分成了 allow_url_fopen 和 allow_url_include 两个指令。如果 allow_url_fopen 操作是禁止的,那么 allow_url_include 也将被禁止。默认情况下将会允许进行 allow_url_fopen 操作,但是禁止allow_url_include 。这样就能非常有效的避免远程代码注入(remote code injection)。这个本来也是打算在 PHP6中添加的,现在我们提前用到了。:D

4、PHP 5.2 增加了对接口中构造函数类型(签名) 强制性检查的支持。

从 PHP 5.2 开始,如果你在一个接口中声明了一个构造函数,那么在所有实现该接口的类都必须包含一个构造函数,并且这个构造函数要与该接口的构造函数的签名完全一致。这里术语“签名”的意思是函数的参数和返回值的类型(包括其语言类型以及是引用传递还是值传递),这个概念有点类似于C 语言中 “原型”。看以下代码:

interface constr {
function __construct() ;
}
class implem implements constr {
function __construct ($a) {
}
}

这段代码在 PHP 5.0和 PHP 5.1 里面运行是毫无问题的,但在 PHP 5.2 中则会抛出一个错误:Fatal error: Declaration of implem::__construct() must be compatible with that of constr::__construct(),提示类implem 的构造函数与接口constr 的构造函数的声明不匹配。

值得一提的是这项新特性的添加过程是很有意思,有兴趣的朋友可以到 Zend 的每周总结(http://www.zend.com/zend/week/week279.php#Heading9)里面看看来龙去脉,此处不再赘述。

5、__toString() 函数将会在任何合适的地方被调用。

魔术方法 __toString() 现在会在一个字符串上下文环境中被调用。换句话说,一个对象在任何地方都可以作为一个字符串来使用,只要它实现了 __toString() 函数。当然你实现的 __toString() 函数不能抛出异常,否则脚本将会中止运行。以前为防万一,PHP 5.0/5.1会在必要时会把对象标识(Id)作为一个字符串返回,这个特性在 PHP 5.2中已经被抛弃。因此这带来的问题就可能是不能保证一个对象的标识总是唯一的。如果你在程序中利用了对象标识符的唯一性,那这将会是某种缺陷。如果没有实现类的 __toString()函数但却把其对象作为作为一个字符串来使用就会导致一个“可捕捉的致命错误”。 还有个特例,就是对象也不能作为作为数组的索引或者键名,即使是它有一个 __toString() 方法。以后 PHP 可能会内建一个 Hash机制来提供对对象唯一性的支持,但就现在来说,你必须自己提供一个对象的 hash 算法,或者干脆就用新提供的 SPL 函数:spl_object_hash();

6、为在写模式下访问 __get() 的返回值这种情况增加了E_NOTICE 级错误提示。

显然 __get() 函数只能在读模式下返回一个值,并且也不可能把一个值写入 __get() 函数。但在以前的版本中并没有为这种不正确的用法给予提示。从 PHP 5.2 开始将会为这种情况抛出一个E_NOTICE。注意:如果你对 foreach() 和其他的一些更改数组内部指针的函数也采取了同样的操作(即给 foreach 所“抽”出来的值进行赋值),那也会触发一个E_NOTICE ,因为这些“抽”出来的值都是处于读模式。如果你的代码中存在这种情况,那你应该把 __get() 函数的返回值转换为一个数组,或者用 SPL 里面的 ArrayObject 来代替这个数组。

7、丢弃了抽象静态的类函数。

由于“笔漏”,PHP 5.0 和 5.1 版本竟然允许类具有抽象静态函数,不过现在不行了。现在只允许接口有抽象静态函数。

8、其他一些语言特性的改变:

  • - SPL 新增了正则迭代器(Regex Iterators)、文件对象(SplFileObject)的CSV 支持等。
  • - 增加了对 RFC2397 (数据流)的支持。
  • - 增加了对Apache 2.2的支持。
  • - 现在可以在上传文件时实时取得文件的上传进度了。
  • - 对PHP或其扩展所需 OpenSSL 库、PCRE 库、MySQL客户端库、PostgreSQL 客户端库、SQLite 库等均进行更新升级。

最后再来介绍一下 PHP 运行模式方面的改动:

1、首先是 PHP 5.2 改变了 Win32 环境下 PHPRC 环境变量的优先级。

    在以前搜索 php.ini 的路径顺序为:

  1. SAPI 模块所指定的位置(Apache 2 中的 PHPIniDir 指令,CGI 和 CLI 中的 -c 命令行选项,NSAPI 中的 php_ini 参数,THTTPD 中的 PHP_INI_PATH 环境变量)
  2. HKEY_LOCAL_MACHINE\SOFTWARE\PHP\IniFilePath(Windows 注册表位置)
  3. PHPRC 环境变量
  4. 当前工作目录(对于 CLI)
  5. web 服务器目录(对于 SAPI 模块)或 PHP 所在目录(Windows 下其它情况)
  6. Windows 目录(C:\windows 或 C:\winnt),或 –with-config-file-path 编译时选项指定的位置现在 PHPRC 环境变量由第三优先权变为第二优先权,高于Windows 注册表所指定的位置。

2、PHP 的命令行模式(CLI SAPI)不再在CWD(当前工作目录)里查找 php.ini 或php-cli.ini 文件。

在 PHP 5.1.x 中有一个未公开的特性就是 CLI 会自动在当前目录中搜索 PHP 的配置文件。这种随便读入一个未经许可的配置文件的行为将可能会导致一个不可预知的错误。在 PHP 5.2 版本里已经将该特性移除,不再再在CWD(当前工作目录)里查找 php.ini 或php-cli.ini 文件了。

总结:

总体来说,从性能上 PHP 5.2 已经超越 PHP4.x 成为目前速度最快的版本,从语言及安全特性上也是无出其右。即便是 PHP4.x 有一些代码缓存工具(如 eAccelerator)可以提高性能,但目前也已经有 APC、X-Cache 等表示可以支持 5.2 版本(eAccelerator 可能还需要一段时间来完善)。虽然 APC 等在性能还略输于eAccelerator,但加上 PHP5.2 的种种优化措施,至少不会比 PHP4+eAccelerator 差到哪去更何况还有很要命的安全更新。既然如此,那我们还有什么理由不升级呢?同时我也推荐各大虚拟主机厂商将其服务器(至少是新增的服务器)更新为 PHP5.2 版本。在不增加性能消耗的基础上还能为客户提供更多的语言特性,同时也增强了产品的竞争力,何乐而不为呢?

让我们开始全面进入 PHP 5.2 的时代吧!

« Previous PageNext Page »