[ 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
仅代表这个变量是否存在而已。
No Comments