深入理解PHP内核(十二)函数-函数的定义、传参及返回值
原文链接:http://www.orlion.ga/344/
一、函数的定义
用户函数的定义从function 关键字开始,如下
function foo($var) { echo $var;
}
1、词法分析
在Zend/zend_language_scanner.l中我们找到如下所示的代码:
<ST_IN_SCRIPTING>"function" { return T_FUNCTION;
}
它所表示的含义是function将会生成T_FUNCTION标记。在获取这个标记后,我们开始语法分析。
2、语法分析
在Zend/zend_language_parser.y文件中找到函数的声明过程标记如下:

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

关注点在function is_reference T_STRING,表示function关键字,是否引用,函数名
T_FUNCTION标记只是用来定位函数的声明,表示这是一个函数,而更多的工作是与这个函数相关的东西,包括参数,返回值。
3、生成中间代码
语法解析后,我们看到所执行编译函数为zend_do_begin_function_declaration。在Zend/zend_complie.c文件找到其实现如下:

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, & **) &

生成的代码为ZEND_DECLARE_FUNCTION,根据这个中间的代码及操作数对应的op_type。我们可以找到中间代码的执行函数为ZEND_DECLARE_FUNCTION_SPEC_HANDLER。
在生成中间代码的时候,可以看到已经统一了函数名全部为小写,表示函数的名称不是区 分大小写的。
为验证这个实现,我们看一段代码

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

执行代码会报错Fatal error: Cannot redeclare t() (previously declared in …)
表示对于PHP来说T和t是同一个函数名,校验函数名是否重复,这个过程是在哪进行的呢?
4、执行中间代码
在Zend/zend_vm_execute.h文件中找到ZEND_DECLARE_FUNCTION中间代码对应的执行函数:ZEND_DECLARE_FUNCTION_SPEC_HANDLER。此函数只调用了函数do_bind_function。其调用代码为:
do_bind_function(EX(opline), EG(function_table), 0);
在这个函数中将EX(opline)所指向的函数添加到EG(function_table)中,并判断是否已经存在相同名字的函数,如果存在则报错,EG(function_table)用来存放执行过程中全部的函数信息,相当于函数的注册表。它的结构是一个HashTable,所以在do_bind_function函数中添加新的函数使用的是HashTable的操作函数zend_hash_add
二、函数的参数
函数的定义只是一个将函数名注册到函数列表的过程。
1、用户自定义函数的参数
我们知道对于函数的参数检查是通过zend_do_receive_arg函数来实现的,在此函数中对于参数的关键代码如下:

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;

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

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;

参数的值传递和参数传递的区别是通过pass_by_reference参数在生成中间代码时实现的。
对于参数的个数,中间代码中包含的arg_nums字段在每次执行**zend_do_receive_argxx时都会加1.如下代码:
CG(active_op_array)->num_args++;
并且当前参数的索引为CG(active_op_array)->num_args-1.如下代码:
cur_arg_info = &CG(active_op_array)->arg_info[CG(active_op_array)->num_args-1];
以上的分析是针对函数定义时的参数设置,这些参数是固定的。而在实际编写程序时可能我们会用到可变参数。此时我们会用到函数func_num_args和func_get_args。它们是以内部函数存在。于是在Zend\zend_builtin_functions.c文件中找到这两个函数的实现。我们首先来看func_num_args函数的实现,其代码如下:

/* {{{ proto int func_num_args(void)
Get the number of arguments that were passed to the function */ZEND_FUNCTION(func_num_args)
{
zend_execute_data *ex = EG(current_execute_data)->prev_execute_data;
if (ex && ex->function_state.arguments) {
RETURN_LONG((long)(zend_uintptr_t)*(ex->function_state.arguments));
} else {
zend_error(E_WARNING,"func_num_args(): Called from the global scope - no function context");
RETURN_LONG(-1);
}
}/* }}} */

在存在ex->function_state.arguments的情况下,及函数调用时,返回ex->function_state.arguments转化后的值,否则显示错误并返回-1。这里最关键的一点是EG(current_execute_data)。这个变量存放的是当前执行程序或函数的数据,此时我们需要取前一个执行程序的数据,为什么呢?因为这个函数的调用是在进入函数后执行的。函数的相关数据等都在之前执行过程中,于是调用的是:
zend_execute_data *ex = EG(current_execute_data)->prev_execute_data;
2、内部函数的参数
以常见的count函数为例,其参数处理部分的代码如下:

/* {{{ proto int count(mixed var [, int mode])
Count the number of elements in a variable (usually an array) */PHP_FUNCTION(count)
{
zval *array; long mode = COUNT_NORMAL;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) { return;
}
... //省略}

这里包括了两个操作:一个是取参数的个数,一个是解析参数列表。
(1)取参数的个数
取参数的个数是通过ZEND_NUM_ARGS()宏来实现的,其定义如下:
#define ZEND_NUM_ARGS() (ht)
ht是在Zend/zend.h文件中定义的宏INTERNAL_FUNCTION_PARAMETERS中的ht,如下
#define INTERNAL_FUNCTION_PARAMETERS int ht, zval *return_value,zval **return_value_ptr, zval *this_ptr, int return_value_used TSRMLS_DC
(2)解析参数列表
PHP内部函数在解析参数时使用的是zend_parse_parameters。它可以大大简化参数的接收处理工作,虽然它在处理可变参数时还有点弱。
其声明如下:
ZEND_API int zend_parse_parameters(int num_args TSRMLS_DC, char *type_spec,
...)
第一个参数num_args表明表示想要接收的参数个数,我们经常使用ZEND_NUM_ARGS()来表示对传入的参数“有多少要多少”
第二个参数应该是宏TSRMLS_CC。
第三个参数type_spec是一个字符串,用来指定我们所期待接收的各个参数的类型,有点类似于printf中指定输出格式的那个格式化字符串。
剩下的参数就是我们用来接收PHP参数值的变量的指针。
zend_parse_parameters()在解析参数的同时户尽可能的转换参数类型,这样就可以确保我们总是能得到所期望的类型的变量
3、函数的返回值
PHP中函数都有返回值,没return返回null
(1)return语句
从Zend/zend_language_parser.y文件中可以确认其生成中间代码调用的是zend_do_return函数。

void zend_do_return(znode *expr, int do_end_vparse TSRMLS_DC) /* {{{ */{
zend_op *opline; int start_op_number, end_op_number; if (do_end_vparse) { if (CG(active_op_array)->return_reference && !zend_is_function_or_method_call(expr)) {
zend_do_end_variable_parse(expr, BP_VAR_W, 0 TSRMLS_CC);/* 处理返回引用 */
} else {
zend_do_end_variable_parse(expr, BP_VAR_R, 0 TSRMLS_CC);/* 处理常规变量返回 */
}
}
...// 省略,取其他中间代码操作
opline->opcode = ZEND_RETURN;
if (expr) {
opline->op1 = *expr;
if (do_end_vparse && zend_is_function_or_method_call(expr)) {
opline->extended_value = ZEND_RETURNS_FUNCTION;
}
} else {
opline->op1.op_type = IS_CONST;
INIT_ZVAL(opline->op1.u.constant);
}
SET_UNUSED(opline->op2);
}/* }}} */

生成中间代码为ZEND_RETURN。第一个操作数的类型在返回值为可用的表达式时,其类型为表达式的操作类型,否则类型为IS_CONST。这在后续计算执行中间代码函数时有用到。根据操作数的不同,ZEND_RETURN中间代码会执行ZEND_RETURN_SPEC_CONST_HANDLER,ZEND_RETURN_SPEC_TMP_HANDLER或ZEND_RETURN_SPEC_TMP_HANDLER。这三个函数的执行流程基本类似,包括对一些错误的处理。这里我们以ZEND_RETURN_SPEC_CONST_HANDLER为例说明函数返回值的执行过程:

static int ZEND_FASTCALL
ZEND_RETURN_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zval *retval_ptr;
zval **retval_ptr_ptr;
if (EG(active_op_array)->return_reference == ZEND_RETURN_REF) {
// ǓǔŷsÁ\ɁƶMļ@ɗÁĻļ
if (IS_CONST == IS_CONST || IS_CONST == IS_TMP_VAR) {
/* Not supposed to happen, but we'll allow it */
zend_error(E_NOTICE, "Only variable references \
should be returned by reference");
goto return_by_value;
}
retval_ptr_ptr = NULL; // ǓǔŔ
if (IS_CONST == IS_VAR && !retval_ptr_ptr) {
zend_error_noreturn(E_ERROR, "Cannot return string offsets by reference"); } if (IS_CONST == IS_VAR && !Z_ISREF_PP(retval_ptr_ptr)) { if (opline->extended_value == ZEND_RETURNS_FUNCTION &&
EX_T(opline->op1.u.var).var.fcall_returned_reference) {
} else if (EX_T(opline->op1.u.var).var.ptr_ptr ==
&EX_T(opline->op1.u.var).var.ptr) { if (IS_CONST == IS_VAR && !0) { /* undo the effect of get_zval_ptr_ptr() */
PZVAL_LOCK(*retval_ptr_ptr);
}
zend_error(E_NOTICE, "Only variable references \
should be returned by reference");
goto return_by_value;
}
}
if (EG(return_value_ptr_ptr)) { // Ǔǔŷs
SEPARATE_ZVAL_TO_MAKE_IS_REF(retval_ptr_ptr); // is_ref__gcőę1
Z_ADDREF_PP(retval_ptr_ptr); // refcount__gcŒď×1
(*EG(return_value_ptr_ptr)) = (*retval_ptr_ptr);
}
} else {
return_by_value:
retval_ptr = &opline->op1.u.constant;
if (!EG(return_value_ptr_ptr)) { if (IS_CONST == IS_TMP_VAR) {
}
} else if (!0) { /* Not a temp var */
if (IS_CONST == IS_CONST ||
EG(active_op_array)->return_reference == ZEND_RETURN_REF ||
(PZVAL_IS_REF(retval_ptr) && Z_REFCOUNT_P(retval_ptr) > 0)) {
zval *ret;
ALLOC_ZVAL(ret);
INIT_PZVAL_COPY(ret, retval_ptr); // ŁͿʍǓǔŔ zval_copy_ctor(ret); *EG(return_value_ptr_ptr) = ret;
} else { *EG(return_value_ptr_ptr) = retval_ptr; // ħ6ɶŔ Z_ADDREF_P(retval_ptr);
}
} else {
zval *ret;
ALLOC_ZVAL(ret);
INIT_PZVAL_COPY(ret, retval_ptr); // ŁͿʍǓǔŔ
*EG(return_value_ptr_ptr) = ret;
}
}
return zend_leave_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS_PASSTHRU); // Ǔǔĉșʒ
}

函数的返回值在程序执行时存储在*EG(return_value_ptr_ptr)。ZEND内核对值返回和引用返回作了区别,并且在此基础上对常量,临时变量和其他类型的变量在返回时作了不同的处理。在return执行完之后,ZEND内核通过调用zend_leave_helper_SPEC函数,清除函数内部使用的变量等。这也是ZEND内核自动给函数加上NULL返回的原因之一。
深入理解PHP内核(十二)函数-函数的定义、传参及返回值的更多相关文章
- 深入理解PHP内核(六)函数的定义、传参及返回值
一.函数的定义 用户函数的定义从function 关键字开始,如下 function foo($var) { echo $var; } 1.词法分析 在Zend/zend_language_scann ...
- arm汇编进入C函数分析,C函数压栈,出栈,传参,返回值
环境及代码介绍 环境和源码 由于有时候要透彻的理解C里面的一些细节问题,所有有必要看看汇编,首先这一切的开始就是从汇编代码进入C的main函数过程.这里不使用编译器自动生成的这部分汇编代码,因为编译器 ...
- python函数知识一 函数初始、定义与调用、返回值、参数和函数的好处+菜中菜
第四章 函数 1.函数初识: def :关键字 -- 定义 函数名:和变量的定义方式一样 (): 用于参数传递,: 形参:函数的定义中()内的是形参 实参:调用的()内是实参 传参:调用时将实参传递给 ...
- python函数传参和返回值注意事项
函数传参 空参数 定义函数时括号里面没有形参,调用时不用传参. def func(): print('null para.') # 调用 func() 位置传参 规定形参的数量,调用时必须传递相同数量 ...
- C#中的函数(二) 有参有返回值的函数
接上一篇 C#中的函数(-) 无参无返回值的函数 http://www.cnblogs.com/fzxiaoyi/p/8502613.html 这次研究下C#中的函数(二) 有参有返回值的函数 依然写 ...
- python oop培训文档里面的 正宗oop、多个函数间反复return传参、多个文件无限复制粘贴扣字、无效废物滑稽类4种方式的例子。(2)
把文档里面说的几种范式发出来. 4种编程范式实现一个人吃喝拉撒长身体的代码.语法很简单,思想模拟了所有程序员写代码时候的代码规划设计想法. 0.我不反对复制粘贴的写法,可以百度搜索复制粘贴网上现有的, ...
- (转)Javascript匿名函数的写法、传参、递归
(原)http://www.veryhuo.com/a/view/37529.html (转)javascript匿名函数的写法.传参和递归 javascript匿名函数的写法.传参和递归 http: ...
- (转)javascript匿名函数的写法、传参和递归
(原)http://www.veryhuo.com/a/view/37529.html (转)javascript匿名函数的写法.传参和递归 http://www.veryhuo.com 2011-0 ...
- C#多线程函数如何传参数和返回值
详见网站:http://WWW.MOVIH.COM就是一个多线程爬虫系统. C#多线程函数如何传参数和返回值 提起多线程,不得不提起 委托(delegates)这个概念. 我理解的委托 ...
随机推荐
- 01.WAMP搭建 [Win7+Apache2.4+MySQL5.7+PHP7
WAMP搭建[Win7+Apache2.4+MySQL5.7+PHP7 一.背景 将电脑光驱位拆换成固态硬盘(120g),专门装了一个系统用于工作.之前一直使用PHPstudy和WAMP这种集成环境, ...
- javascriptone
var start=Date.now();document.write("<h1>This is a paragraph.</h1>");function ...
- Erlang在Windows上开发环境搭建全过程讲解目录
我会按照下面的列表来一步一步讲解,在windows来开发Erlang所用到的一些工具,和知识.我会不停的添加和修正. Erlang运行时环境 Erlang开发工具选择 Rebar来构建,编译,测试,发 ...
- NTP时间同步
1.设置NTP相关服务开机自启: # chkconfig ntpd on # chkconfig ntpdate on # chkconfig |grep ntp 2.启动NTP服务: # servi ...
- flashcache
Flashcache系统管理员手册 1.简介Flashcache 是为Linux操作系统开发的块设备回写缓存内核模块,使用了Device Mapper的方式实现,本文档可指导你快速掌握Flashcac ...
- Version history of VC++, MFC and ATL
I have tried to assemble together information about the Visual C++ releases, the compiler and the fr ...
- .net开发笔记(十二) 设计时与运行时的区别(续)
上一篇博客详细讲到了设计时(DesignTime)和运行时(RunTime)的概念与区别,不过没有给出实际的Demo,今天整理了一下,做了一个例子,贴出来分享一下,巩固前一篇博客讲到的内容. 简单回顾 ...
- Ng Http Request/response格式转换
angular作为Single Page Application推荐的交互方式当然是基于json的ajax调用.但今天要说的是当你不幸工作在一个遗留或者不可控制的服务上,而这服务是基于非json提交方 ...
- vi 卡住怎么办
http://zhidao.baidu.com/question/93283359.html ---------- 我貌似好像按了ctrl+s... 用惯了windows人,会习惯性地按Ctrl+S来 ...
- 每天一个linux命令(54):ping命令
Linux系统的ping命令是常用的网络命令,它通常用来测试与目标主机的连通性,我们经常会说“ping一下某机器,看是不是开着”.不能打开网页时会说“你先ping网关地址192.168.1.1试试”. ...