PHP实现强类型函数返回值

澳门新葡亰娱乐在线 1

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

原文链接:

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

一、函数的定义

比如下面的代码:

  用户函数的定义从function 关键字开始,如下

<?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.
….
}
?>

function foo($var) {    echo $var;
}

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

  1、词法分析

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

  在Zend/zend_language_scanner.l中我们找到如下所示的代码:

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

<ST_IN_SCRIPTING>"function" {    return T_FUNCTION;
}

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

  它所表示的含义是function将会生成T_FUNCTION标记。在获取这个标记后,我们开始语法分析。

澳门新葡亰娱乐在线 1

  2、语法分析

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

  在Zend/zend_language_parser.y文件中找到函数的声明过程标记如下:

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

澳门新葡亰娱乐在线 2

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

function:
    T_FUNCTION { $$.u.opline_num = CG(zend_lineno); }
;
 
is_reference:        /* empty */ { $$.op_type = ZEND_RETURN_VAL; }    |   '&'         { $$.op_type = ZEND_RETURN_REF; }
;
 
unticked_function_declaration_statement:
        function is_reference T_STRING {
zend_do_begin_function_declaration(&$1, &$3, 0, $2.op_type, NULL TSRMLS_CC); }            '(' parameter_list ')' '{' inner_statement_list '}' {
                zend_do_end_function_declaration(&$1 TSRMLS_CC); }
;

详情,这里就不讲了,

澳门新葡亰娱乐在线 3

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

    关注点在function is_reference
T_STRING,表示function关键字,是否引用,函数名

增加如下代码:

  T_FUNCTION标记只是用来定位函数的声明,表示这是一个函数,而更多的工作是与这个函数相关的东西,包括参数,返回值。

<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;
}

  3、生成中间代码

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

  语法解析后,我们看到所执行编译函数为zend_do_begin_function_declaration。在Zend/zend_complie.c文件找到其实现如下:

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

澳门新葡亰娱乐在线 4

增加如下代码

 zend_do_begin_function_declaration(znode ** is_method,  return_reference, znode *fn_flags_znode TSRMLS_DC) 
    function_token->u.op_array ==== &=
 
    „!*opline =->opcode =->op1.op_type =&opline->->op2.op_type =->op2.u.constant.type =->op2.u.constant.value.str.val =->op2.u.constant.value.str.len =->op2.u.constant, ->extended_value =-
>->op1.u.constant.value.str.len, & **) &

……….
%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;
}

澳门新葡亰娱乐在线 5

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

  生成的代码为ZEND_DECLARE_FUNCTION,根据这个中间的代码及操作数对应的op_type。我们可以找到中间代码的执行函数为ZEND_DECLARE_FUNCTION_SPEC_HANDLER。

这样语法解释器就可以处理我们新的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;
……….

澳门新葡亰娱乐在线 6

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

function T() {
    echo 1;
}
 
function t() {
    echo 2;
}

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

澳门新葡亰娱乐在线 7

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

  执行代码会报错Fatal error: Cannot redeclare t() (previously declared
in …)

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

  表示对于PHP来说T和t是同一个函数名,校验函数名是否重复,这个过程是在哪进行的呢?

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

  4、执行中间代码

ZEND_RETURN_SPEC_CONST_HANDLER
ZEND_RETURN_SPEC_TMP_HANDLER
ZEND_澳门新葡亰娱乐在线,RETURN_SPEC_VAR_HANDLER

  在Zend/zend_vm_execute.h文件中找到ZEND_DECLARE_FUNCTION中间代码对应的执行函数:ZEND_DECLARE_FUNCTION_SPEC_HANDLER。此函数只调用了函数do_bind_function。其调用代码为:

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

do_bind_function(EX(opline), EG(function_table), 0);

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

  在这个函数中将EX(opline)所指向的函数添加到EG(function_table)中,并判断是否已经存在相同名字的函数,如果存在则报错,EG(function_table)用来存放执行过程中全部的函数信息,相当于函数的注册表。它的结构是一个HashTable,所以在do_bind_function函数中添加新的函数使用的是HashTable的操作函数zend_hash_add

在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版本,有需要的可以拿去玩一玩。

  1、用户自定义函数的参数

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

  我们知道对于函数的参数检查是通过zend_do_receive_arg函数来实现的,在此函数中对于参数的关键代码如下:

下载补丁:php-syntax.patch

澳门新葡亰娱乐在线 8

CG(active_op_array)->arg_info = erealloc(CG(active_op_array)->arg_info,        sizeof(zend_arg_info)*(CG(active_op_array)->num_args));
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];
cur_arg_info->name = estrndup(varname->u.constant.value.str.val,
        varname->u.constant.value.str.len);
cur_arg_info->name_len = varname->u.constant.value.str.len;
cur_arg_info->array_type_hint = 0;
cur_arg_info->allow_null = 1;
cur_arg_info->pass_by_reference = pass_by_reference;
cur_arg_info->class_name = NULL;
cur_arg_info->class_name_len = 0;

澳门新葡亰娱乐在线 9

  整个参数的传递是通过给中间代码的arg_info字段执行赋值操作完成。关键点是在arg_info字段,arg_info字段的结构如下:

澳门新葡亰娱乐在线 10

typedef struct _zend_arg_info {    const char *name;   /*参数的名称*/
    zend_uint name_len;     /*参数名称的长度*/
    const char *class_name; /* 类名*/
     zend_uint class_name_len;   /*类名长度*/
    zend_bool array_type_hint;  /*数组类型提示*/
    zend_bool allow_null;   /*是否允许为NULLͺ*/
    zend_bool pass_by_reference;    /*是否引用传递*/
    zend_bool return_reference; 
    int required_num_args;  
} zend_arg_info;

发表评论

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

相关文章

网站地图xml地图