【php7扩展开发四】函数的参数 ,引用传参 ,返回值

函数参数解析
之前我们定义的函数没有接收任何参数,那么扩展定义的内部函数如何读取参数呢?用户自定义函数在编译时会为每个参数创建一个zend_arg_info结构,这个结构用来记录参数的名称、是否引用传参、是否为可变参数等,在存储上函数参数与局部变量相同,都分配在zend_execute_data上,且最先分配的就是函数参数,调用函数时首先会进行参数传递,按参数次序依次将参数的value从调用空间传递到被调函数的zend_execute_data,函数内部像访问普通局部变量一样通过存储位置访问参数,这是用户自定义函数的参数实现。

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


PHP中通过 zend_parse_parameters() 这个函数解析zend_execute_data上保存的参数:

zend_parse_parameters(int num_args, const char *type_spec, ...);


num_args为实际传参数,通过 ZEND_NUM_ARGS()获取;
type_spec是一个字符串,用来标识解析参数的类型,比如:"la"表示第一个参数为整形,第二个为数组,将按照这个解析到指定变量;
后面是一个可变参数,用来指定解析到的变量,这个值与type_spec配合使用,即type_spec用来指定解析的变量类型,可变参数用来指定要解析到的变量,这个值必须是指针。
解析的过程也比较容易理解,调用函数时首先会把参数拷贝到调用函数的zend_execute_data上,所以解析的过程就是按照type_spec指定的各个类型,依次从zend_execute_data上获取参数,然后将参数地址赋给目标变量。

参数类型

整形:l、L

整形通过"l"或"L"标识,表示解析的参数为整形,解析到的变量类型必须是 zend_long ,不能解析其它类型,如果输入的参数不是整形将按照类型转换规则将其转为整形:

zend_long lval;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l", &lval){
...
}
printf("lval:%d\n", lval);


如果在标识符后加"!",即:"l!"、"L!",则必须再提供一个zend_bool变量的地址,通过这个值可以判断传入的参数是否为NULL,如果为NULL则将要解析到的zend_long值设置为0,同时zend_bool设置为1:

zend_long lval; //如果参数为NULL则此值被设为0
zend_bool is_null;//如果参数为NULL则此值为1,否则为0
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l!", &lval, &is_null){
..
}


布尔型:b

通过"b"标识符表示将传入的参数解析为布尔型,解析到的变量必须是zend_bool:

zend_bool ok;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "b", &ok, &is_null) == FAILURE){
..
}


"b!"的用法与整形的完全相同,也必须再提供一个zend_bool的地址用于获取传参是否为NULL,如果为NULL,则zend_bool为0,用于获取是否NULL的zend_bool为1。


浮点型:d
通过"d"标识符表示将参数解析为浮点型,解析的变量类型必须为double:

double dval;
 
if(zend_parse_parameters(ZEND_NUM_ARGS(), "d", &dval) == FAILURE){
..
}


具体解析过程不再展开,"d!"与整形、布尔型用法完全相同。


字符串:s、S、p、P
字符串解析有两种形式:char、zend_string,其中"s"将参数解析到`char`,且需要额外提供一个size_t类型的变量用于获取字符串长度,"S"将解析到zend_string:

char *str;
size_t str_len;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "s", &str, &str_len) == FAILURE){
...
}
zend_string *str;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "S", &str) == FAILURE){
...
}


"s!"、"S!"与整形、布尔型用法不同,字符串时不需要额外提供zend_bool的地址,如果参数为NULL,则char*、zend_string将设置为NULL。除了"s"、"S"之外还有两个类似的:"p"、"P",从解析规则来看主要用于解析路径,实际与普通字符串没什么区别,尚不清楚这俩有什么特殊用法。

数组:a、A、h、H
数组的解析也有两类,一类是解析到zval层面,另一类是解析到HashTable,其中"a"、"A"解析到的变量必须是zval,"h"、"H"解析到HashTable,这两类是等价的:

zval *arr; //必须是zval指针,不能是zval arr,因为参数保存在zend_execute_data上,arr为此空间上参数的地址
HashTable *ht;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "ah", &arr, &ht) == FAILURE){
...
}


"a!"、"A!"、"h!"、"H!"的用法与字符串一致,也不需要额外提供别的地址,如果传参为NULL,则对应解析到的zval、HashTable也为NULL.

对象:o、O
如果参数是一个对象则可以通过"o"、"O"将其解析到目标变量,注意:只能解析为zval,无法解析为zend_object。

zval *obj;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "o", &obj) == FAILURE){
...
}


O"是要求解析指定类或其子类的对象,类似传参时显式的声明了参数类型的用法function my_func(MyClass $obj){...} ,如果参数不是指定类的实例化对象则无法解析。
"o!"、"O!"与字符串用法相同

资源:r
如果参数为资源则可以通过"r"获取其zval的地址,但是无法直接解析到zend_resource的地址,与对象相同。

zval *res;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "r", &res) == FAILURE){
...
}


"r!"与字符串用法相。

类:C
如果参数是一个类则可以通过"C"解析出zend_class_entry地址:function my_func(stdClass){...} ,这里有个地方比较特殊,解析到的变量可以设定为一个类,这种情况下解析时将会找到的类与指定的类之间的父子关系,只有存在父子关系才能解析,如果只是想根据参数获取类型的zend_class_entry地址,记得将解析到的地址初始化为NULL,否则将会不可预料的错误。

zend_class_entry *ce = NULL; //初始为NULL
if(zend_parse_parameters(ZEND_NUM_ARGS(), "C", &ce) == FAILURE){
RETURN_FALSE;
}


callable:f
callable指函数或成员方法,如果参数是函数名称字符串、array(对象/类,成员方法),则可以通过"f"标识符解析出 zend_fcall_info 结构,这个结构是调用函数、成员方法时的唯一输入。

zend_fcall_info callable; //注意,这两个结构不能是指针
zend_fcall_info_cache call_cache;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "f", &callable, &call_cache) ==FAILURE){
RETURN_FALSE;
}


函数调用:

my_func_1("func_name");//或
my_func_1(array('class_name', 'static_method'));//或
my_func_1(array($object, 'method'));


解析出 zend_fcall_info 后就可以通过 zend_call_function() 调用函数、成员方法了,提供"f"解析到 zend_fcall_info 的用意是简化函数调用的操作,否则需要我们自己去查找函数、检查是否可被调用等工作,关于这个结构稍后介绍函数调用时再作详细说明。

任意类型:z
"z"表示按参数实际类型解析,比如参数为字符串就解析为字符串,参数为数组就解析为数组,这种实际就是将zend_execute_data上的参数地址拷贝到目的变量了,没有做任何转化。
"z!"与字符串用法相同。
 

引用传参
函数中解析参数还有一种就是引用传参。

如果函数需要使用引用类型的参数或返回引用就需要创建函数的参数数组,这个数组通过:

ZEND_BEGIN_ARG_INFO()或ZEND_BEGIN_ARG_INFO_EX() 、
ZEND_END_ARG_INFO() 宏定义:

#define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args)
#define ZEND_BEGIN_ARG_INFO(name, _unused)


name: 参数数组名,注册函数 PHP_FE(function, arg_info) 会用到
_unused: 保留值,暂时无用
__return_reference:__ 返回值是否为引用,一般很少会用到
required_num_args: required参数数
这两个宏需要与 ZEND_END_ARG_INFO() 配合使用:

ZEND_BEGIN_ARG_INFO_EX(arginfo_my_func_1, 0, 0, 2)
...
ZEND_END_ARG_INFO


 
接着就是在上面两个宏中间定义每一个参数的zend_internal_arg_info,PHP提供的宏有:

//pass_by_ref表示是否引用传参,name为参数名称
#define ZEND_ARG_INFO(pass_by_ref, name) { #name, NULL, 0, pass_by_ref, 0, 0 },
 
//只声明此参数为引用传参
#define ZEND_ARG_PASS_INFO(pass_by_ref) { NULL, NULL, 0, pass_by_ref, 0, 0 },
 
//显式声明此参数的类型为指定类的对象,等价于PHP中这样声明:MyClass $obj
#define ZEND_ARG_OBJ_INFO(pass_by_ref, name, classname, allow_null) { #name, #classname, IS_OBJECT, pass_by_ref, allow_null, 0 },
 
//显式声明此参数类型为数组,等价于:array $arr
#define ZEND_ARG_ARRAY_INFO(pass_by_ref, name, allow_null) { #name, NULL, IS_ARRAY, pass_by_ref, allow_null, 0 },
 
//显式声明为callable,将检查函数、成员方法是否可调
#define ZEND_ARG_CALLABLE_INFO(pass_by_ref, name, allow_null) { #name, NULL, IS_CALLABLE, pass_by_ref, allow_null, 0 },
 
//通用宏,自定义各个字段
#define ZEND_ARG_TYPE_INFO(pass_by_ref, name, type_hint, allow_null) { #name, NULL, type_hint, pass_by_ref, allow_null, 0 },
 
//声明为可变参数
#define ZEND_ARG_VARIADIC_INFO(pass_by_ref, name) { #name, NULL, 0, pass_by_ref, 0, 1 },


看个例子:

function my_func_1(&$a, Exception $c){..}


用内核实现则可以这么定义:

ZEND_BEGIN_ARG_INFO_EX(arginfo_my_func_1, 0, 0, 1)
    ZEND_ARG_INFO(1, a) //引用
    ZEND_ARG_OBJ_INFO(0, b, Exception, 0) //注意:这里不要把字符串加""
ZEND_END_ARG_INFO()


展开后:

static const zend_internal_arg_info name[] = {
    //多出来的这个是给返回值用的
    { (const char*)(zend_uintptr_t)(2), NULL, 0, 0, 0, 0 },
    { "a", NULL, 0, 0, 0, 0 },
    { "b", "Exception", 8, 1, 0, 0 },
}


第一个数组元素用于记录必传参数的数量以及返回值是否为引用。定义完这个数组接下来就需要把这个数组告诉函数:

const zend_function_entry mytest_functions[] = {
    PHP_FE(my_func_1, arginfo_my_func_1)
    PHP_FE(my_func_2, NULL)
    PHP_FE_END //末尾必须加这个
};


引用参数通过 zend_parse_parameters() 解析时只能使用"z"解析,不能再直接解析为zend_value了,否则引用将失效:

PHP_FUNCTION(my_func_1)
{
    zval *lval; //必须为zval,定义为zend_long也能解析出,但不是引用
    zval *obj;
    if(zend_parse_parameters(ZEND_NUM_ARGS(), "zo", &lval, &obj) == FAILURE){
        RETURN_FALSE;
    }
    //lval的类型为IS_REFERENCE
    zval *real_val = Z_REFVAL_P(lval); //获取实际引用的zval地址:&(lval.value->ref.val)
    Z_LVAL_P(real_val) = 100; //设置实际引用的类型
}


 

$a = 90;
$b = new Exception;
my_func_1($a, $b);
echo $a; //100


函数返回值
调用内部函数时其返回值指针作为参数传入,这个参数为 zval *return_value ,如果函数有返回值直接设置此指针即可,需要特别注意的是设置返回值时需要增加其引用计数,举个例子来看:

PHP_FUNCTION(my_func_1)
{
    zval *arr;
    if(zend_parse_parameters(ZEND_NUM_ARGS(), "a", &arr) == FAILURE){
        RETURN_FALSE;
    }
    //增加引用计数
    Z_ADDREF_P(arr);
    //设置返回值为数组:
    ZVAL_ARR(return_value, Z_ARR_P(arr));
}


此函数接收一个数组,然后直接返回该数组,相当于:

function my_func_1($arr){
    return $arr;
}


虽然可以直接设置return_value,但实际使用时并不建议这么做,因为PHP提供了很多专门用于设置返回值的宏,这些宏定义在 zend_API.h 中:

//返回布尔型,b:IS_FALSE、IS_TRUE
#define RETURN_BOOL(b) { RETVAL_BOOL(b); return; }
 
//返回NULL
#define RETURN_NULL() { RETVAL_NULL(); return;}
 
//返回整形,l类型:zend_long
#define RETURN_LONG(l) { RETVAL_LONG(l); return; }
 
//返回浮点值,d类型:double
#define RETURN_DOUBLE(d) { RETVAL_DOUBLE(d); return; }
 
//返回字符串,可返回内部字符串,s类型为:zend_string *
#define RETURN_STR(s) { RETVAL_STR(s); return; }
 
//返回内部字符串,这种变量将不会被回收,s类型为:zend_string *
#define RETURN_INTERNED_STR(s) { RETVAL_INTERNED_STR(s); return;
}
 
//返回普通字符串,非内部字符串,s类型为:zend_string *
#define RETURN_NEW_STR(s) { RETVAL_NEW_STR(s); return; }
 
//拷贝字符串用于返回,这个会自己加引用计数,s类型为:zend_string *
#define RETURN_STR_COPY(s) { RETVAL_STR_COPY(s); return; }
 
//返回char *类型的字符串,s类型为char *
#define RETURN_STRING(s) { RETVAL_STRING(s); return; }
 
//返回char *类型的字符串,s类型为char *,l为字符串长度,类型为size_t
#define RETURN_STRINGL(s, l) { RETVAL_STRINGL(s, l); return; }
 
//返回空字符串
#define RETURN_EMPTY_STRING() { RETVAL_EMPTY_STRING(); return;
}
 
//返回资源,r类型:zend_resource *
#define RETURN_RES(r) { RETVAL_RES(r); return; }
 
//返回数组,r类型:zend_array *
#define RETURN_ARR(r) { RETVAL_ARR(r); return; }
 
//返回对象,r类型:zend_object *
#define RETURN_OBJ(r) { RETVAL_OBJ(r); return; }
 
//返回zval
#define RETURN_ZVAL(zv, copy, dtor) { RETVAL_ZVAL(zv, copy, dtor); re
turn; }
 
//返回false
#define RETURN_FALSE { RETVAL_FALSE; return; }
 
//返回true
#define RETURN_TRUE { RETVAL_TRUE; return;}
 

猜你喜欢

转载自blog.csdn.net/zhangge3663/article/details/115173324