[ PHP 内核与扩展开发系列] 变量在内核中的实现 —— PHP 变量的创建、存储和检索

创建变量

我们已经知道 PHP 变量在内核中其实是通过 zval 结构来实现的,也初步了解如何设置一个 zval 结构的类型和值。这一节我们将在前两节的基础上,彻底掌握对 zval 结构的操控,期间将引入很多超棒的新宏。

在编码的时候,很希望在内核中创建的 zval 可以让用户在 PHP 语言里以变量的形式使用,为了实现这个功能,我们首先要创建一个 zval。最容易想到的办法便是创建一个 zval 指针,然后申请一块内存并让指针指向它。如果你脑海里浮现出了 malloc(sizeof(zval)) 的影子,那么请你立即刹车,不要用malloc 来做这件事情,内核给我们提供了相应的宏来处理这件事,理由和以前一样:为了代码漂亮并保持版本升级时的兼容性。这个宏是:MAKE_STD_ZVAL(pzv)。这个宏会用内核的方式来申请一块内存并将其地址赋给 pzv,并初始化它的 refcountis_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);strlen 两个参数很好理解,因为我们知道内核中保存了字符串的地址和它的长度,后面的 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_RESOURCEZVAL_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_findEG(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 常量。

就去符号表里找变量而言,SUCCESSFAILURE 仅代表这个变量是否存在而已。

学院君 has written 699 articles

资深PHP工程师,Laravel学院院长

发表评论

标记为*的字段是必填项(邮箱地址不会被公开)

你可以使用这些HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>