[ PHP 内核与扩展开发系列] 变量在内核中的实现 —— PHP 变量的创建、存储和检索
创建变量
我们已经知道 PHP 变量在内核中其实是通过
zval 结构来实现的,也初步了解如何设置一个
zval 结构的类型和值。这一节我们将在前两节的基础上,彻底掌握对
zval 结构的操控,期间将引入很多超棒的新宏。
在编码的时候,很希望在内核中创建的
zval 可以让用户在 PHP 语言里以变量的形式使用,为了实现这个功能,我们首先要创建一个
zval。最容易想到的办法便是创建一个
zval 指针,然后申请一块内存并让指针指向它。如果你脑海里浮现出了
malloc(sizeof(zval)) 的影子,那么请你立即刹车,不要用
malloc 来做这件事情,内核给我们提供了相应的宏来处理这件事,理由和以前一样:为了代码漂亮并保持版本升级时的兼容性。这个宏是:
MAKE_STD_ZVAL(pzv)。这个宏会用内核的方式来申请一块内存并将其地址赋给
pzv,并初始化它的
refcount 和
is_ref 两个属性,更棒的是,它不但会自动处理内存不足问题,还会在内存中选择最优的位置来申请。
除了
MAKE_STD_ZVAL() 宏函数,
ALLOC_INIT_ZVAL() 宏函数也是用来干这件事的,唯一的不同是它会将
pzv 所指的
zval 的类型设置为
IS_NULL。
申请完空间后,我们便可以给这个
zval 赋值了。基于已经介绍的宏,也许我们需要
Z_TYPE_P(p) = IS_NULL 来设置其是
NULL 类型,并通过
Z_SOMEVAL 形式的宏来为它赋值,但是现在你有了更好更简单的选择。
内核中提供一些宏来简化我们的操作,可以只用一步便设置好
zval 的类型和值。
| 新宏 |
其它宏的实现方法 |
|---|
| ZVAL_NULL(pvz); (注意这个Z和VAL之间没有下划线!) |
Z_TYPE_P(pzv) = IS_NULL; |
| ZVAL_BOOL(pzv, b); (将pzv所指的zval设置为IS_BOOL类型,值是b) |
Z_TYPE_P(pzv) = IS_BOOL;
Z_BVAL_P(pzv) = b ? 1 : 0; |
| ZVAL_TRUE(pzv); (将pzv所指的zval设置为IS_BOOL类型,值是true) |
ZVAL_BOOL(pzv, 1); |
| ZVAL_FALSE(pzv); (将pzv所指的zval设置为IS_BOOL类型,值是false) |
ZVAL_BOOL(pzv, 0); |
| ZVAL_LONG(pzv, l); (将pzv所指的zval设置为IS_LONG类型,值是l) |
Z_TYPE_P(pzv) = IS_LONG;
Z_LVAL_P(pzv) = l; |
| ZVAL_DOUBLE(pzv, d); (将pzv所指的zval设置为IS_DOUBLE类型,值是d) |
Z_TYPE_P(pzv) = IS_DOUBLE;
Z_DVAL_P(pzv) = d; |
| ZVAL_STRINGL(pzv, str, len, dup); |
Z_TYPE_P(pzv) = IS_STRING;
Z_STRLEN_P(pzv) = len;
if (dup) {
Z_STRVAL_P(pzv) = estrndup(str, len + 1);
} else {
Z_STRVAL_P(pzv) = str;
}
|
| ZVAL_STRING(pzv, str, dup); |
ZVAL_STRINGL(pzv, str, strlen(str), dup); |
| ZVAL_RESOURCE(pzv, res); |
Z_TYPE_P(pzv) = IS_RESOURCE;
Z_RESVAL_P(pzv) = res; |
ZVAL_STRINGL(pzv,str,len,dup) 中的 dup 参数
先阐述一下
ZVAL_STRINGL(pzv,str,len,dup);,
str 和
len 两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的
dup 的意思其实很简单,它指明了该字符串是否需要被复制,值为
1 将先申请一块新内存并复制该字符串,然后把新内存的地址赋值给
pzv, 为
0 时则直接把
str 的地址赋值给
zval。
这项特性将会在你仅仅需要创建一个变量并将其指向一个已经由 Zend 内部数据内存时很有用。
ZVAL_STRINGL 与 ZVAL_STRING 的区别
如果你想在某一位置截取该字符串或已经知道了这个字符串的长度,那么可以使用宏
ZVAL_STRINGL(zval, string, length, duplicate) ,它显式的指定字符串长度,而不是使用
strlen()。这个宏将该字符串长度作为参数,但它是二进制安全的,而且速度也比
ZVAL_STRING 快,因为少了个
strlen。
ZVAL_RESOURCE 约等于 ZVAL_LONG
上一节中我们说过 PHP 中的资源类型的值其实就是一个整数,所以
ZVAL_RESOURCE 和
ZVAL_LONG 的工作差不多, 只不过它会把
zval 的类型设置为
IS_RESOURCE。
变量存储
我们在前面两节已经了解了 PHP 中变量的类型和值是怎样在内核中用 C 语言实现的,这一节我们将看一下内核是怎样来组织用户在 PHP 中定义的变量的。
有一点对我们扩展开发者来说非常棒,那就是用户在 PHP 中定义的变量我们都可以在一个
HashTable 中找到,当 PHP 中定义了一个变量,内核会自动的把它的信息储存到一个用
HashTable 实现的符号表里。
全局作用域的符号表是在调用扩展的
RINIT 方法(一般都是
MINIT 方法里)前创建的,并在
RSHUTDOWN 方法执行后自动销毁。
当用户在 PHP 中调用一个函数或者类的方法时,内核会创建一个新的符号表并激活之, 这也就是为什么我们无法在函数中使用在函数外定义的变量的原因 (因为它们分属两个符号表,一个当前作用域的,一个全局作用域的)。如果不是在一个函数里,则全局作用域的符号表处于激活状态。
我们现在打开
Zend/zend_globals.h 文件,看一下
_zend_execution_globals 结构体,会在其中发现这么两个元素:
struct _zend_executor_globals {
...
HashTable symbol_table;
HashTable *active_symbol_table;
...
};
其中的
symbol_table 元素可以通过
EG 宏来访问,它代表着 PHP 的
全局变量,如
$GLOBALS,其实从根本上来讲,
$GLOBALS 不过是
EG(symbol_table) 的一层封装而已。
与之对应,下面的
active_symbol_table 元素也可以通过
EG(active_symbol_table) 的方法来访问,它代表的是处于当前作用域的变量符号表(
局部变量)。
我们上边也看到了,其实这两个成员在
_zend_executor_globals 里虽然都代表
HashTable, 但一个是真正的
HashTable,而另一个是一个指针。当我们在对
HashTable 进行操作的时候,往往是把它的地址传递给一些函数。所以,如果我们要对
EG(symbol_table) 的结果进行操作,往往需要对它进行求址操作然后用它的地址作为被调用函数的参数。
下面我们用一段例子来解释下上面说的理论:
<?php
$foo = 'bar';
?>
上面是一段 PHP 语言的例子,我们创建了一个变量,并把它的值设置为
'bar',在以后的代码中我们便可以使用
$foo 变量。相同的功能我们怎样在内核中实现呢?我们可以先构思一下步骤:
- 创建一个
zval 结构,并设置其类型。
- 设置值为
'bar'。
- 将其加入当前作用域的符号表,只有这样用户才能在 PHP 里使用这个变量。
具体的代码为:
{
zval *fooval;
MAKE_STD_ZVAL(fooval);
ZVAL_STRING(fooval, "bar", 1);
ZEND_SET_SYMBOL(EG(active_symbol_table), "foo", fooval);
}
首先,我们声明一个
zval 指针,并申请一块内存。然后通过
ZVAL_STRING 宏将值设置为
'bar',最后一行的作用就是将这个
zval 加入到当前的符号表里去,并将其
label 定义成
foo,这样用户就可以在代码里通过
$foo 来使用它了。
变量检索
用户在 PHP 语言里定义的变量,我们能否在内核中获取到呢?答案当然是肯定的,下面我们就看如何通过
zend_hash_find() 函数来找到当前某个作用域下用户已经定义好的变量。
zend_hash_find() 函数是内核提供的操作
HashTable 的API之一,如果你没有接触过,可以先记住怎么使用就可以了。
{
zval **fooval;
if (zend_hash_find(
EG(active_symbol_table), //这个参数是地址,如果我们操作全局作用域,则需要&EG(symbol_table)
"foo",
sizeof("foo"),
(void**)&fooval
) == SUCCESS
)
{
php_printf("成功发现$foo!");
}
else
{
php_printf("当前作用域下无法发现$foo.");
}
}
首先我们定义了一个指向指针的指针,然后通过
zend_hash_find 去
EG(active_symbol_table) 作用域下寻找名称为
foo($foo) 的变量,如果成功找到,此函数将返回
SUCCESS。
看完代码,你肯定有很多疑问。为什么还要进行
sizeof("foo") 运算,
fooval 明明是
zval** 型的,为什么转成
void** 的?而且为什么还要进行
&fooval 运算,
fooval 本身不就已经是指向指针的指针了吗?:-) 问题确实很多,不要过于担心,让我们带着这些问题继续往下走。
首先要说明的是,内核定义
HashTable 这个结构,并不是单单用来储存 PHP 语言里的变量的,其它很多地方都在应用
HashTable (这就是个神器)。一个
HashTable 有很多元素,在内核里叫做
bucket。然而每个
bucket 的大小是固定的,所以如果我们想在
bucket 里存储任意数据时,最好的办法便是申请一块内存保存数据,然后在
bucket 里保存它的指针。以
zval *foo 为例,内核会先申请一块足够保存指针的内存来保存
foo,比如这块内存的地址是
p,也就是
p=&foo,并在
bucket 里保存
p,这时我们便明白了,
p 其实就是
zval ** 类型的。至于
bucket 为什么保存
zval ** 类型的指针,而不是直接保存
zval * 类型的指针,我们到下一章再详细叙述。
所以当我们去
HashTable 里寻找变量的时候,得到的值其实是一个
zval 的指针。
如果
zend_hash_find() 函数找到了我们需要的数据,它将返回
SUCCESS 常量, 并把它的地址赋给我们在调用
zend_hash_find() 函数传递的
fooval 参数,也就是说此时
fooval 就指向了我们要找的数据。如果没有找到,那它不会对我们
fooval 参数做任何修改,并返回
FAILURE 常量。
就去符号表里找变量而言,
SUCCESS 和
FAILURE 仅代表这个变量是否存在而已。
无评论