漫谈php全局变量Global

在开发过程中,函数的返回值类型应该是确定不变的,但PHP是弱类型的语言,

global语句的作用是定义全局变量,例如如果想在函数内访问全局作用域内的变量则可以通过global声明来定义。

所以PHP是没有此类语法验证的,正因为如此,造成了很多坑坑。

 

比如下面的代码:

下面从语法解释开始分析。

<?php
function getArticles(…){
$arrData = array();
if($exp1){
return $arrData;
}else if($exp2){
return 1;
}else{
return false;
}

}
$arrData =getArticles(…);
foreach($arrData as $record){
//do something.
….
}
?>

 

函数getArticles根据不同的条件返回不同类型的值,有bool、int、还有数组,正常情况这类函数是希望返回数组,然后拿数组去做一些其他操作,

1. 词法解析

查看
Zend/zend_language_scanner.l文件,搜索
global关键字。我们可以找到如下代码:

<ST_IN_SCRIPTING>”global”
{
return T_GLOBAL;
}

可因为函数返回值类型不固定,调用时就很可能产生各种预想不到的坑,

2. 语法解析

在词法解析完后,获得了token,此时通过这个token,我们去Zend/zend_language_parser.y文件中查找。找到相关代码如下:

|   T_GLOBAL global_var_list ‘;’
 
global_var_list:
    global_var_list ‘,’ global_var  {
zend_do_fetch_global_variable(&$3, NULL, ZEND_FETCH_GLOBAL_LOCK
TSRMLS_CC); }
|   global_var                      {
zend_do_fetch_global_variable(&$1, NULL, ZEND_FETCH_GLOBAL_LOCK
TSRMLS_CC); }
;

上面代码中的$3是指global_var(如果不清楚yacc的语法,可以查阅yacc入门类的文章。)

从上面的代码可以知道,对于全局变量的声明调用的是zend_do_fetch_global_variable函数,查找此函数的实现在Zend/zend_compile.c文件。

void
zend_do_fetch_global_variable(znode *varname, const znode
*static_assignment, int fetch_type TSRMLS_DC) 
{
        …//省略
        opline->opcode = ZEND_FETCH_W;      /* the default mode
must be Write, since fetch_simple_variable() is used to define
function arguments */
        opline->result.op_type = IS_VAR;
        opline->result.u.EA.type = 0;
        opline->result.u.var =
get_temporary_variable(CG(active_op_array));
        opline->op1 = *奥门新浦京官方网站 ,varname;
        SET_UNUSED(opline->op2);
        opline->op2.u.EA.type = fetch_type;
        result = opline->result;
 
        … // 省略
        fetch_simple_variable(&lval, varname, 0 TSRMLS_CC); /*
Relies on the fact that the default fetch is BP_VAR_W */
 
        zend_do_assign_ref(NULL, &lval, &result TSRMLS_CC);
       
CG(active_op_array)->opcodes[CG(active_op_array)->last-1].result.u.EA.type
|= EXT_TYPE_UNUSED;
}
/* }}} */

上面的代码确认了opcode为ZEND_FETCH_W外,还执行了zend_do_assign_ref函数。zend_do_assign_ref函数的实现如下:

void
zend_do_assign_ref(znode *result, const znode *lvar, const znode
*rvar TSRMLS_DC) /* {{{ */
{
        zend_op *opline;
 
       … //省略
 
        opline = get_next_op(CG(active_op_array) TSRMLS_CC);
        opline->opcode = ZEND_ASSIGN_REF;
       …//省略
        if (result) {
                opline->result.op_type = IS_VAR;
                opline->result.u.EA.type = 0;
                opline->result.u.var =
get_temporary_variable(CG(active_op_array));
                *result = opline->result;
        } else {
                /* SET_UNUSED(opline->result); */
                opline->result.u.EA.type |= EXT_TYPE_UNUSED;
        }
        opline->op1 = *lvar;
        opline->op2 = *rvar;
}

从上面的zend_do_fetch_global_variable函数和zend_do_assign_ref函数的实现可以看出,
使用global声明一个全局变量后,其执行了两步操作,ZEND_FETCH_W和ZEND_ASSIGN_REF。

因此我就想,既然不能规范,那直接强制好了。

3. 生成并执行中间代码

我们看下ZEND_FETCH_W的最后执行。从代码中我们可以知道:

  • ZEND_FETCH_W = 83
  • op->op1.op_type = 4
  • op->op2.op_type = 0

而计算最后调用的方法在代码中的体现为:

zend_opcode_handlers[opcode
* 25 + zend_vm_decode[op->op1.op_type] * 5 +
zend_vm_decode[op->op2.op_type]];

计算,最后调用ZEND_FETCH_W_SPEC_CV_HANDLER函数。即

static
int ZEND_FASTCALL
 ZEND_FETCH_W_SPEC_CV_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
return zend_fetch_var_address_helper_SPEC_CV(BP_VAR_W,
ZEND_OPCODE_HANDLER_ARGS_PASSTHRU);
}

在zend_fetch_var_address_helper_SPEC_CV中调用如下代码获取符号表

target_symbol_table
= zend_get_target_symbol_table(opline, EX(Ts), type, varname
TSRMLS_CC);

在zend_get_target_symbol_table函数的实现如下:

static
inline HashTable *zend_get_target_symbol_table(const zend_op
*opline, const temp_variable *Ts, int type, const zval *variable
TSRMLS_DC)
{
        switch (opline->op2.u.EA.type) {
                … //  省略
                case ZEND_FETCH_GLOBAL:
                case ZEND_FETCH_GLOBAL_LOCK:
                        return &EG(symbol_table);
                        break;
               …  //  省略
        }
        return NULL;
}

在前面语法分析过程中,程序传递的参数是
ZEND_FETCH_GLOBAL_LOCK,于是如上所示。我们取&EG(symbol_table);的值。这也是全局变量的存放位置。

如上就是整个global的解析过程。

函数/方法返回值可以强制类型,如 图

奥门新浦京官方网站 1

支持四种强制类型限制:int、array、bool、object,当返回值与函数声明中的类型不匹配时,抛出warning,本来想抛出error,但是觉得

太狠了,只能算是个异常,不能算错误,所以就用warning好了。

PHP本身是不支持 int function
这样的语法的,所以要支持,就先要搞定语法解析器,关于语法解析器,可以移步这里>>>查看

详情,这里就不讲了,

先修改语法扫描 Zend/zend_language_scanner.l文件

增加如下代码:

<ST_IN_SCRIPTING>”int” {
return T_FUNCTION_RETURN_INT;
}
<ST_IN_SCRIPTING>”bool” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”object” {
return T_FUNCTION_RETURN_OBJECT;
}
<ST_IN_SCRIPTING>”resource” {
return T_FUNCTION_RETURN_RESOURCE;
}

意思很简单,扫描器扫描到到关键字
int、bool、object、resource、array时返回相应的T_FUNCTION_*
,这是一个token,

scanner根据不同的token做不同的处理,token要先在Zend/zend_language_parser.y文件中定义

增加如下代码

……….
%token T_FUNCTION_RETURN_INT
%token T_FUNCTION_RETURN_BOOL
%token T_FUNCTION_RETURN_STRING
%token T_FUNCTION_RETURN_OBJECT
%token T_FUNCTION_RETURN_RESOURCE
1

然后增加token处理逻辑:

1
function:
T_FUNCTION { $$.u.opline_num = CG(zend_lineno);$$.u.EA.var  = 0;
}
|   T_FUNCTION_RETURN_INT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_LONG;
}
|   T_FUNCTION_RETURN_BOOL T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_BOOL;
}
|   T_FUNCTION_RETURN_STRING T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_STRING;
}
|   T_FUNCTION_RETURN_OBJECT T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_OBJECT;
}
|   T_FUNCTION_RETURN_RESOURCE T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_RESOURCE;
}
|   T_ARRAY T_FUNCTION {
$$.u.opline_num = CG(zend_lineno);
$$.u.EA.var = IS_ARRAY;
}

$$.u.EA.var 存储的是 函数返回类型,最后要拿他来跟返回值类型做匹配,

这样语法解释器就可以处理我们新的php语法了。

这还不够,还需要修改函数声明定义的处理逻辑

Zend/zend_compile.c ::zend_do_begin_function_declaration

……
zend_op_array op_array;
char *name = function_name->u.constant.value.str.val;
int name_len = function_name->u.constant.value.str.len;
int function_type  = function_token->u.EA.var;
//保存函数类型,在语法解释器中增加的: $$.u.EA.var = IS_LONG;
int function_begin_line = function_token->u.opline_num;
……
op_array.function_name = name;
op_array.fn_type = function_type; //将类型保存到op_array中,
op_array.return_reference = return_reference;
op_array.fn_flags |= fn_flags;
op_array.pass_rest_by_reference = 0;
……….

PHP是先解析PHP语法生成相应的opcode,将需要的环境、参数信息保存到execute_data全局变量中,最后在通过execute函数逐条执行opcode,

所以要做处理就要把函数的类型保存到opcode中:op_array.fn_type =
function_type;

op_array是没有fn_type的,要修改op_array的结构,增加zend_uint
fn_type;

(关于opcode你可以想象一下
从c转为汇编,我博客中也有相关文章,可以参考一下)

最后要修改opcode的毁掉函数,函数的返回 return 会生成token
T_RETURN,T_RETURN会根据返回的类型调用不同的calback函数:

ZEND_RETURN_SPEC_CONST_HANDLER
ZEND_RETURN_SPEC_TMP_HANDLER
ZEND_RETURN_SPEC_VAR_HANDLER

它有三个callback,如果返回值是一个 const类型的数据,则
ZEND_RETURN_SPEC_CONST_HANDLER
返回值是临时数据,如 : return 1,则ZEND_RETURN_SPEC_TMP_HANDLER
返回值是一个变量,如 : return $a,则ZEND_RETURN_SPEC_VAR_HANDLER

所以要在这三个callback函数中增加处理逻辑:

在callback函数return之前增加如下代码

if((EG(active_op_array)->fn_type > 0) &&
Z_TYPE_P(retval_ptr) != EG(active_op_array)->fn_type){
php_error_docref0(NULL TSRMLS_DC,E_WARNING, “function name %s
return a wrong type.”, EG(active_op_array)->function_name );
}

fn_type 去跟 返回值的类型作比较,如果没有匹配到,就会抛出这个warning。

我已经打了补丁,目前只支持php5.3版本,有需要的可以拿去玩一玩。

不清楚为什么官方不支持此语法,我觉得还是挺有必要的。

下载补丁:php-syntax.patch

发表评论

电子邮件地址不会被公开。 必填项已用*标注