3.2 函数实现

函数,通俗的讲就是一组操作的集合,给予特定的输入将对应特定的输出。

3.2.1 用户自定义函数的实现

用户自定义函数是指我们在PHP脚本通过function定义的函数:

  1. function my_func(){
  2. ...
  3. }

汇编中函数对应的是一组独立的汇编指令,然后通过call指令实现函数的调用。前面已经说过PHP编译的结果是opcode数组,与汇编指令对应。PHP用户自定义函数的实现就是将函数编译为独立的opcode数组,调用时分配独立的执行栈依次执行opcode,所以自定义函数对于zend而言并没有什么特别之处,只是将opcode进行了打包封装。PHP脚本中函数之外的指令,整个可以认为是一个函数(或者理解为main函数更直观)。

  1. /* function main(){ */
  2. $a = 123;
  3. echo $a;
  4. /* } */

3.2.1.1 函数的存储结构

下面具体看下PHP中函数的结构:

  1. typedef union _zend_function zend_function;
  2. //zend_compile.h
  3. union _zend_function {
  4. zend_uchar type; /* MUST be the first element of this struct! */
  5. struct {
  6. zend_uchar type; /* never used */
  7. zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
  8. uint32_t fn_flags;
  9. zend_string *function_name;
  10. zend_class_entry *scope; //成员方法所属类,面向对象实现中用到
  11. union _zend_function *prototype;
  12. uint32_t num_args; //参数数量
  13. uint32_t required_num_args; //必传参数数量
  14. zend_arg_info *arg_info; //参数信息
  15. } common;
  16. zend_op_array op_array; //函数实际编译为普通的zend_op_array
  17. zend_internal_function internal_function;
  18. };

这是一个union,因为PHP中函数除了用户自定义函数还有一种:内部函数,内部函数是通过扩展或者内核提供的C函数,比如time、array系列等等,内部函数稍后再作分析。

内部函数主要用到internal_function,而用户自定义函数编译完就是一个普通的opcode数组,用的是op_array(注意:op_array、internal_function是嵌入的两个结构,而不是一个单独的指针),除了这两个上面还有一个typecommon,这俩是做什么用的呢?

经过比较发现zend_op_arrayzend_internal_function结构的起始位置都有common中的几个成员,如果你对C的内存比较了解应该会马上想到它们的用法,实际common可以看作是op_arrayinternal_function的header,不管是什么哪种函数都可以通过zend_function.common.xx快速访问到zend_function.zend_op_array.xxzend_function.zend_internal_function.xx,下面几个,type同理,可以直接通过zend_function.type取到zend_function.op_array.typezend_function.internal_function.type

php function

函数是在编译阶段确定的,那么它们存在哪呢?

在PHP脚本的生命周期中有一个非常重要的值executor_globals(非ZTS下),类型是struct _zend_executor_globals,它记录着PHP生命周期中所有的数据,如果你写过PHP扩展一定用到过EG这个宏,这个宏实际就是对executor_globals的操作:define EG(v) (executor_globals.v)

EG(function_table)是一个哈希表,记录的就是PHP中所有的函数。

PHP在编译阶段将用户自定义的函数编译为独立的opcodes,保存在EG(function_table)中,调用时重新分配新的zend_execute_data(相当于运行栈),然后执行函数的opcodes,调用完再还原到旧的zend_execute_data,继续执行,关于zend引擎execute阶段后面会详细分析。

3.2.1.2 函数参数

函数参数在内核实现上与函数内的局部变量实际是一样的,上一篇我们介绍编译的时候提供局部变量会有一个单独的 编号 ,而函数的参数与之相同,参数名称也在zend_op_array.vars中,编号首先是从参数开始的,所以按照参数顺序其编号依次为0、1、2…(转化为相对内存偏移量就是96、112、128…),然后函数调用时首先会在调用位置将参数的value复制到各参数各自的位置,详细的传参过程我们在执行一篇再作说明。

比如:

  1. function my_function($a, $b = "aa"){
  2. $ret = $a . $b;
  3. return $ret;
  4. }

编译完后各变量的内存偏移量编号:

  1. $a => 96
  2. $b => 112
  3. $ret => 128

与下面这么写一样:

  1. function my_function(){
  2. $a = NULL;
  3. $b = "aa";
  4. $ret = $a . $b;
  5. return $ret;
  6. }

另外参数还有其它的信息,这些信息通过zend_arg_info结构记录:

  1. typedef struct _zend_arg_info {
  2. zend_string *name; //参数名
  3. zend_string *class_name;
  4. zend_uchar type_hint; //显式声明的参数类型,比如(array $param_1)
  5. zend_uchar pass_by_reference; //是否引用传参,参数前加&的这个值就是1
  6. zend_bool allow_null; //是否允许为NULL,注意:这个值并不是用来表示参数是否为必传的
  7. zend_bool is_variadic; //是否为可变参数,即...用法,与golang的用法相同,5.6以上新增的一个用法:function my_func($a, ...$b){...}
  8. } zend_arg_info;

每个参数都有一个上面的结构,所有参数的结构保存在zend_op_array.arg_info数组中,这里有一个地方需要注意:zend_op_array->arg_info数组保存的并不全是输入参数,如果函数声明了返回值类型则也会为它创建一个zend_arg_info,这个结构在arg_info数组的第一个位置,这种情况下zend_op_array->arg_info指向的实际是数组的第二个位置,返回值的结构通过zend_op_array->arg_info[-1]读取,这里先单独看下编译时的处理:

  1. //函数参数的编译
  2. void zend_compile_params(zend_ast *ast, zend_ast *return_type_ast)
  3. {
  4. zend_ast_list *list = zend_ast_get_list(ast);
  5. uint32_t i;
  6. zend_op_array *op_array = CG(active_op_array);
  7. zend_arg_info *arg_infos;
  8. if (return_type_ast) {
  9. //声明了返回值类型:function my_func():array{...}
  10. //多分配一个zend_arg_info
  11. arg_infos = safe_emalloc(sizeof(zend_arg_info), list->children + 1, 0);
  12. ...
  13. arg_infos->allow_null = 0;
  14. ...
  15. //arg_infos指向了下一个位置
  16. arg_infos++;
  17. op_array->fn_flags |= ZEND_ACC_HAS_RETURN_TYPE;
  18. } else {
  19. //没有声明返回值类型
  20. if (list->children == 0) {
  21. return;
  22. }
  23. arg_infos = safe_emalloc(sizeof(zend_arg_info), list->children, 0);
  24. }
  25. ...
  26. op_array->num_args = list->children;
  27. //声明了返回值的情况下arg_infos已经指向了数组的第二个元素
  28. op_array->arg_info = arg_infos;
  29. }

3.2.1.3 函数的编译

我们在上一篇文章介绍过PHP代码的编译过程,主要是PHP->AST->Opcodes的转化,上面也说了函数其实就是将一组PHP代码编译为单独的opcodes,函数的调用就是不同opcodes间的切换,所以函数的编译过程与普通PHP代码基本一致,只是会有一些特殊操作,我们以3.2.1.2开始那个例子简单看下编译过程。

普通函数的语法解析规则:

  1. function_declaration_statement:
  2. function returns_ref T_STRING backup_doc_comment '(' parameter_list ')' return_type
  3. '{' inner_statement_list '}'
  4. { $$ = zend_ast_create_decl(ZEND_AST_FUNC_DECL, $2, $1, $4,
  5. zend_ast_get_str($3), $6, NULL, $10, $8); }
  6. ;

规则主要由五部分组成:

  • returns_ref: 是否返回引用,在函数名前加&,比如function &test(){…}
  • T_STRING: 函数名
  • parameter_list: 参数列表
  • return_type: 返回值类型
  • inner_statement_list: 函数内部代码

函数生成的抽象语法树根节点类型是zend_ast_decl,所有函数相关的信息都记录在这个节点中(除了函数外类也是用的这个):

  1. typedef struct _zend_ast_decl {
  2. zend_ast_kind kind; //函数就是ZEND_AST_FUNC_DECL,类则是ZEND_AST_CLASS
  3. zend_ast_attr attr; /* Unused - for structure compatibility */
  4. uint32_t start_lineno; //函数起始行
  5. uint32_t end_lineno; //函数结束行
  6. uint32_t flags; //其中一个标识位用来标识返回值是否为引用,是则为ZEND_ACC_RETURN_REFERENCE
  7. unsigned char *lex_pos;
  8. zend_string *doc_comment;
  9. zend_string *name; //函数名
  10. zend_ast *child[4]; //child有4个子节点,分别是:参数列表节点、use列表节点、函数内部表达式节点、返回值类型节点
  11. } zend_ast_decl;

上面的例子最终生成的语法树:

3.2 函数实现 - 图2

具体编译为opcodes的过程在zend_compile_func_decl()中:

  1. void zend_compile_func_decl(znode *result, zend_ast *ast)
  2. {
  3. zend_ast_decl *decl = (zend_ast_decl *) ast;
  4. zend_ast *params_ast = decl->child[0]; //参数列表
  5. zend_ast *uses_ast = decl->child[1]; //use列表
  6. zend_ast *stmt_ast = decl->child[2]; //函数内部
  7. zend_ast *return_type_ast = decl->child[3]; //返回值类型
  8. zend_bool is_method = decl->kind == ZEND_AST_METHOD; //是否为成员函数
  9. //这里保存当前正在编译的zend_op_array:CG(active_op_array),然后重新为函数生成一个新的zend_op_array,
  10. //函数编译完再将旧的还原
  11. zend_op_array *orig_op_array = CG(active_op_array);
  12. zend_op_array *op_array = zend_arena_alloc(&CG(arena), sizeof(zend_op_array)); //新分配zend_op_array
  13. ...
  14. if (is_method) {
  15. zend_bool has_body = stmt_ast != NULL;
  16. zend_begin_method_decl(op_array, decl->name, has_body);
  17. } else {
  18. zend_begin_func_decl(result, op_array, decl); //注意这里会在当前zend_op_array(不是新生成的函数那个)生成一条ZEND_DECLARE_FUNCTION的opcode
  19. }
  20. CG(active_op_array) = op_array;
  21. ...
  22. zend_compile_params(params_ast, return_type_ast); //编译参数
  23. if (uses_ast) {
  24. zend_compile_closure_uses(uses_ast);
  25. }
  26. zend_compile_stmt(stmt_ast); //编译函数内部语法
  27. ...
  28. pass_two(CG(active_op_array));
  29. ...
  30. CG(active_op_array) = orig_op_array; //还原之前的
  31. }

编译过程主要有这么几个处理:

(1) 保存当前正在编译的zendoparray,新分配一个结构,因为每个函数、include的文件都对应独立的一个zend_op_array,通过CG(active_op_array)记录当前编译所属zend_op_array,所以开始编译函数时就需要将这个值保存下来,等到函数编译完成再还原回去;另外还有一个关键操作:zend_begin_func_decl,这里会在当前zend_op_array(不是新生成的函数那个)生成一条 __ZEND_DECLARE_FUNCTION 的opcode,也就是函数声明操作。

  1. $a = 123; //当前为CG(active_op_array) = zend_op_array_1,编译到这时此opcode加到zend_op_array_1
  2. //新分配一个zend_op_array_2,并将当前CG(active_op_array)保存到origin_op_array,
  3. //然后将CG(active_op_array)=zend_op_array_2
  4. function test(){
  5. $b = 234; //编译到zend_op_array_2
  6. }//函数编译结束,将CG(active_op_array) = origin_op_array,切回zend_op_array_1
  7. $c = 345; //编译到zend_op_array_1

(2) 编译参数列表,函数的参数我们在上一小节已经介绍,完整的参数会有三个组成:参数类型(可选)、参数名、默认值(可选),这三部分分别保存在参数节点的三个child节点中,编译参数的过程有两个关键操作:

操作1: 为每个参数编号

操作2: 每个参数生成一条opcode,如果是可变参数其opcode=ZEND_RECV_VARIADIC,如果有默认值则为ZEND_RECV_INIT,否则为ZEND_RECV

上面的例子中$a编号为96,$b为112,同时生成了两条opcode:ZEND_RECV、ZEND_RECV_INIT,调用的时候会根据具体传参数量跳过部分opcode,比如这个函数我们这么调用my_function($a)则ZEND_RECV这条opcode就直接跳过了,然后执行ZEND_RECV_INIT将默认值写到112位置,具体的编译过程在zend_compile_params()中,上面已经介绍过。

参数默认值的保存与普通变量赋值相同:$a = array()array()保存在literals,参数的默认值也是如此。

(3) 编译函数内部语法,这个跟普通PHP代码编译过程无异。

(4) pass_two(),上一篇介绍过,不再赘述。

最终生成两个zend_op_array:

3.2 函数实现 - 图3

总体来看,PHP在逐行编译时发现一个function则生成一条ZEND_DECLARE_FUNCTION的opcode,然后调到函数中编译函数,编译完再跳回去继续下面的编译,这里多次提到ZEND_DECLARE_FUNCTION这个opcode是因为在函数编译结束后还有一个重要操作:zend_do_early_binding(),前面我们说过总的编译入口在zend_compile_top_stmt(),这里会对每条语法逐条编译,而函数、类在编译完成后还有后续的操作:

  1. void zend_compile_top_stmt(zend_ast *ast)
  2. {
  3. ...
  4. if (ast->kind == ZEND_AST_STMT_LIST) {
  5. for (i = 0; i < list->children; ++i) {
  6. zend_compile_top_stmt(list->child[i]);
  7. }
  8. }
  9. zend_compile_stmt(ast); //编译各条语法,函数也是在这里编译完成
  10. //函数编译完成后
  11. if (ast->kind == ZEND_AST_FUNC_DECL || ast->kind == ZEND_AST_CLASS) {
  12. CG(zend_lineno) = ((zend_ast_decl *) ast)->end_lineno;
  13. zend_do_early_binding();
  14. }
  15. }

zend_do_early_binding()核心工作就是 将function、class加到CG(function_table)、CG(class_table)中 ,加入成功了就直接把 ZEND_DECLARE_FUNCTION 这条opcode干掉了,加入失败的话则保留,这个相当于 有一部分opcode在『编译时』提前执行了 ,这也是为什么PHP中可以先调用函数再声明函数的原因,比如:

  1. $a = 1234;
  2. echo my_function($a);
  3. function my_function($a){
  4. ...
  5. }

实际原始的opcode以及执行顺序:

3.2 函数实现 - 图4

类的情况也是如此,后面我们再作说明。

3.2.1.4 匿名函数

匿名函数(Anonymous functions),也叫闭包函数(closures),允许临时创建一个没有指定名称的函数。最经常用作回调函数(callback)参数的值。当然,也有其它应用的情况。

官网的示例:

  1. $greet = function($name)
  2. {
  3. printf("Hello %s\r\n", $name);
  4. };
  5. $greet('World');
  6. $greet('PHP');

这里提匿名函数只是想说明编译函数时那个use的用法:

匿名函数可以从父作用域中继承变量。 任何此类变量都应该用 use 语言结构传递进去。

  1. $message = 'hello';
  2. $example = function () use ($message) {
  3. var_dump($message);
  4. };
  5. $example();

3.2.2 内部函数

上一节已经提过,内部函数指的是由内核、扩展提供的C语言编写的function,这类函数不需要经历opcode的编译过程,所以效率上要高于PHP用户自定义的函数,调用时与普通的C程序没有差异。

Zend引擎中定义了很多内部函数供用户在PHP中使用,比如:define、defined、strlen、method_exists、class_exists、function_exists……等等,除了Zend引擎中定义的内部函数,PHP扩展中也提供了大量内部函数,我们也可以灵活的通过扩展自行定制。

3.2.2.1 内部函数结构

上一节介绍zend_function为union,其中internal_function就是内部函数用到的,具体结构:

  1. //zend_complie.h
  2. typedef struct _zend_internal_function {
  3. /* Common elements */
  4. zend_uchar type;
  5. zend_uchar arg_flags[3]; /* bitset of arg_info.pass_by_reference */
  6. uint32_t fn_flags;
  7. zend_string* function_name;
  8. zend_class_entry *scope;
  9. zend_function *prototype;
  10. uint32_t num_args;
  11. uint32_t required_num_args;
  12. zend_internal_arg_info *arg_info;
  13. /* END of common elements */
  14. void (*handler)(INTERNAL_FUNCTION_PARAMETERS); //函数指针,展开:void (*handler)(zend_execute_data *execute_data, zval *return_value)
  15. struct _zend_module_entry *module;
  16. void *reserved[ZEND_MAX_RESERVED_RESOURCES];
  17. } zend_internal_function;

zend_internal_function头部是一个与zend_op_array完全相同的common结构。

下面看下如何定义一个内部函数。

3.2.2.2 定义与注册

内部函数与用户自定义函数冲突,用户无法在PHP代码中覆盖内部函数,执行PHP脚本时会提示error错误。

内部函数的定义非常简单,我们只需要创建一个普通的C函数,然后创建一个zend_internal_function结构添加到 EG(function_table) (也可能是CG(functiontable),取决于在哪一阶段注册)中即可使用,内部函数 _通常 情况下是在php_module_startup阶段注册的,这里之所以说通常是按照标准的扩展定义,除了扩展提供的方式我们可以在任何阶段自由定义内部函数,当然并不建议这样做。下面我们先不讨论扩展标准的定义方式,我们先自己尝试下如何注册一个内部函数。

根据zend_internal_function的结构我们知道需要定义一个handler:

  1. void qp_test(INTERNAL_FUNCTION_PARAMETERS)
  2. {
  3. printf("call internal function 'qp_test'\n");
  4. }

然后创建一个内部函数结构(我们在扩展PHP_MINIT_FUNCTION方法中注册,也可以在其他位置):

  1. PHP_MINIT_FUNCTION(xxxxxx)
  2. {
  3. zend_string *lowercase_name;
  4. zend_function *reg_function;
  5. //函数名转小写,因为php的函数不区分大小写
  6. lowercase_name = zend_string_alloc(7, 1);
  7. zend_str_tolower_copy(ZSTR_VAL(lowercase_name), "qp_test", 7);
  8. lowercase_name = zend_new_interned_string(lowercase_name);
  9. reg_function = malloc(sizeof(zend_internal_function));
  10. reg_function->internal_function.type = ZEND_INTERNAL_FUNCTION; //定义类型为内部函数
  11. reg_function->internal_function.function_name = lowercase_name;
  12. reg_function->internal_function.handler = qp_test;
  13. zend_hash_add_ptr(CG(function_table), lowercase_name, reg_function); //注册到CG(function_table)符号表中
  14. }

接着编译、安装扩展,测试:

  1. qp_test();

结果输出: call internal function 'qp_test'

这样一个内部函数就定义完成了。这里有一个地方需要注意的我们把这个函数注册到 CG(function_table) 中去了,而不是 EG(function_table) ,这是因为在php_request_startup阶段会把 CG(function_table) 赋值给 EG(function_table)

上面的过程看着比较简单,但是在实际应用中不要这样做,PHP提供给我们一套标准的定义方式,接下来看下如何在扩展中按照官方方式提供一个内部函数。

首先也是定义C函数,这个通过PHP_FUNCTION宏定义:

  1. PHP_FUNCTION(qp_test)
  2. {
  3. printf("call internal function 'qp_test'\n");
  4. }

然后是注册过程,这个只需要我们将所有的函数数组添加到扩展结构zend_module_entry.functions即可,扩展加载过程中会自动进行函数注册(见1.2节),不需要我们干预:

  1. const zend_function_entry xxxx_functions[] = {
  2. PHP_FE(qp_test, NULL)
  3. PHP_FE_END
  4. };
  5. zend_module_entry xxxx_module_entry = {
  6. STANDARD_MODULE_HEADER,
  7. "扩展名称",
  8. xxxx_functions,
  9. PHP_MINIT(timeout),
  10. PHP_MSHUTDOWN(timeout),
  11. PHP_RINIT(timeout), /* Replace with NULL if there's nothing to do at request start */
  12. PHP_RSHUTDOWN(timeout), /* Replace with NULL if there's nothing to do at request end */
  13. PHP_MINFO(timeout),
  14. PHP_TIMEOUT_VERSION,
  15. STANDARD_MODULE_PROPERTIES
  16. };

关于更多扩展中函数相关的用法会在后面扩展开发一章中详细介绍,这里不再展开。