7.6.2 函数参数解析
最后更新于:2022-04-02 05:18:38
### 7.6.2 函数参数解析
上面我们定义的函数没有接收任何参数,那么扩展定义的内部函数如何读取参数呢?首先回顾下函数参数的实现:用户自定义函数在编译时会为每个参数创建一个`zend_arg_info`结构,这个结构用来记录参数的名称、是否引用传参、是否为可变参数等,在存储上函数参数与局部变量相同,都分配在zend_execute_data上,且最先分配的就是函数参数,调用函数时首先会进行参数传递,按参数次序依次将参数的value从调用空间传递到被调函数的zend_execute_data,函数内部像访问普通局部变量一样通过存储位置访问参数,这是用户自定义函数的参数实现。
内部函数与用户自定义函数最大的不同在于内部函数就是一个普通的C函数,除函数参数以外在zend_execute_data上没有其他变量的分配,函数参数是从PHP用户空间传到函数的,它们与用户自定义函数完全相同,包括参数的分配方式、传参过程,也是按照参数次序依次分配在zend_execute_data上,所以在扩展中定义的函数直接按照顺序从zend_execute_data上读取对应的值即可,PHP中通过`zend_parse_parameters()`这个函数解析zend_execute_data上保存的参数:
```c
zend_parse_parameters(int num_args, const char *type_spec, ...);
```
* num_args为实际传参数,通过`ZEND_NUM_ARGS()`获取:zend_execute_data->This.u2.num_args,前面曾介绍过`zend_execute_data->This`这个zval的用途;
* type_spec是一个字符串,用来标识解析参数的类型,比如:"la"表示第一个参数为整形,第二个为数组,将按照这个解析到指定变量;
* 后面是一个可变参数,用来指定解析到的变量,这个值与type_spec配合使用,即type_spec用来指定解析的变量类型,可变参数用来指定要解析到的变量,这个值必须是指针。
i解析的过程也比较容易理解,调用函数时首先会把参数拷贝到调用函数的zend_execute_data上,所以解析的过程就是按照type_spec指定的各个类型,依次从zend_execute_data上获取参数,然后将参数地址赋给目标变量,比如下面这个例子:
```c
PHP_FUNCTION(my_func_1)
{
zend_long lval;
zval *arr;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "la", &lval, &arr) == FAILURE){
RETURN_FALSE;
}
...
}
```
对应的内存关系:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/98a33899f3c71d8a2e3e1746853c434a_630x380.png)
注意:解析时除了整形、浮点型、布尔型是直接硬拷贝value外,其它解析到的变量只能是指针,arr为zend_execute_data上param_1的地址,即:`zval *arr = ¶m_1`,也就是说参数始终存储在zend_execute_data上,解析获取的是这些参数的地址。`zend_parse_parameters()`调用了`zend_parse_va_args()`进行处理,简单看下解析过程:
```c
//va就是定义的要解析到的各个变量的地址
static int zend_parse_va_args(int num_args, const char *type_spec, va_list *va, int flags)
{
const char *spec_walk;
int min_num_args = -1; //最少参数数
int max_num_args = 0; //要解析的参数总数
int post_varargs = 0;
zval *arg;
int arg_count; //实际传参数
//遍历type_spec计算出min_num_args、max_num_args
for (spec_walk = type_spec; *spec_walk; spec_walk++) {
...
}
...
//检查数目是否合法
if (num_args < min_num_args || (num_args > max_num_args && max_num_args >= 0)) {
...
}
//获取实际传参数:zend_execute_data.This.u2.num_args
arg_count = ZEND_CALL_NUM_ARGS(EG(current_execute_data));
...
i = 0;
//逐个解析参数
while (num_args-- > 0) {
...
//获取第i个参数的zval地址:arg就是在zend_execute_data上分配的局部变量
arg = ZEND_CALL_ARG(EG(current_execute_data), i + 1);
//解析第i个参数
if (zend_parse_arg(i+1, arg, va, &type_spec, flags) == FAILURE) {
if (varargs && *varargs) {
*varargs = NULL;
}
return FAILURE;
}
i++;
}
}
```
接下来详细看下不同类型的解析方式。
#### 7.6.2.1 整形:l、L
整形通过"l"、"L"标识,表示解析的参数为整形,解析到的变量类型必须是`zend_long`,不能解析其它类型,如果输入的参数不是整形将按照类型转换规则将其转为整形:
```c
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:
```c
zend_long lval; //如果参数为NULL则此值被设为0
zend_bool is_null; //如果参数为NULL则此值为1,否则为0
if(zend_parse_parameters(ZEND_NUM_ARGS(), "l!", &lval, &is_null){
...
}
```
具体的解析过程:
```c
//zend_API.c #line:519
case 'l':
case 'L':
{
//这里获取解析到的变量地址取的是zend_long *,所以只能解析到zend_long
zend_long *p = va_arg(*va, zend_long *);
zend_bool *is_null = NULL;
//后面加"!"时check_null为1
if (check_null) {
is_null = va_arg(*va, zend_bool *);
}
if (!zend_parse_arg_long(arg, p, is_null, check_null, c == 'L')) {
return "integer";
}
}
```
```c
static zend_always_inline int zend_parse_arg_long(zval *arg, zend_long *dest, zend_bool *is_null, int check_null, int cap)
{
if (check_null) {
*is_null = 0;
}
if (EXPECTED(Z_TYPE_P(arg) == IS_LONG)) {
//传参为整形,无需转化
*dest = Z_LVAL_P(arg);
} else if (check_null && Z_TYPE_P(arg) == IS_NULL) {
//传参为NULL
*is_null = 1;
*dest = 0;
} else if (cap) {
//"L"的情况
return zend_parse_arg_long_cap_slow(arg, dest);
} else {
//"l"的情况
return zend_parse_arg_long_slow(arg, dest);
}
return 1;
}
```
> __Note:__ "l"与"L"的区别在于,当传参不是整形且转为整形后超过了整形的大小范围时,"L"将值调整为整形的最大或最小值,而"l"将报错,比如传的参数是字符串"9223372036854775808"(0x7FFFFFFFFFFFFFFF + 1),转整形后超过了有符号int64的最大值:0x7FFFFFFFFFFFFFFF,所以如果是"L"将解析为0x7FFFFFFFFFFFFFFF。
#### 7.6.2.2 布尔型:b
通过"b"标识符表示将传入的参数解析为布尔型,解析到的变量必须是zend_bool:
```c
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。
#### 7.6.2.3 浮点型:d
通过"d"标识符表示将参数解析为浮点型,解析的变量类型必须为double:
```c
double dval;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "d", &dval) == FAILURE){
...
}
```
具体解析过程不再展开,"d!"与整形、布尔型用法完全相同。
#### 7.6.2.4 字符串:s、S、p、P
字符串解析有两种形式:char*、zend_string,其中"s"将参数解析到`char*`,且需要额外提供一个size_t类型的变量用于获取字符串长度,"S"将解析到zend_string:
```c
char *str;
size_t str_len;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "s", &str, &str_len) == FAILURE){
...
}
```
```c
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",从解析规则来看主要用于解析路径,实际与普通字符串没什么区别,尚不清楚这俩有什么特殊用法。
#### 7.6.2.5 数组:a、A、h、H
数组的解析也有两类,一类是解析到zval层面,另一类是解析到HashTable,其中"a"、"A"解析到的变量必须是zval,"h"、"H"解析到HashTable,这两类是等价的:
```c
zval *arr; //必须是zval指针,不能是zval arr,因为参数保存在zend_execute_data上,arr为此空间上参数的地址
HashTable *ht;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "ah", &arr, &ht) == FAILURE){
...
}
```
具体解析过程:
```c
case 'A':
case 'a':
{
//解析到zval *
zval **p = va_arg(*va, zval **);
if (!zend_parse_arg_array(arg, p, check_null, c == 'A')) {
return "array";
}
}
break;
case 'H':
case 'h':
{
//解析到HashTable *
HashTable **p = va_arg(*va, HashTable **);
if (!zend_parse_arg_array_ht(arg, p, check_null, c == 'H')) {
return "array";
}
}
break;
```
"a!"、"A!"、"h!"、"H!"的用法与字符串一致,也不需要额外提供别的地址,如果传参为NULL,则对应解析到的zval*、HashTable*也为NULL。
> __Note:__
>
> 1、"a"与"A"当传参为数组时没有任何差别,它们的区别在于:如果传参为对象"A"将按照对象解析到zval,而"a"将报错
>
> 2、"h"与"H"当传参为数组时同样没有差别,当传参为对象时,"H"将把对象的成员参数数组解析到目标变量,"h"将报错
#### 7.6.2.6 对象:o、O
如果参数是一个对象则可以通过"o"、"O"将其解析到目标变量,注意:只能解析为zval*,无法解析为zend_object*。
```c
zval *obj;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "o", &obj) == FAILURE){
...
}
```
"O"是要求解析指定类或其子类的对象,类似传参时显式的声明了参数类型的用法:`function my_func(MyClass $obj){...}`,如果参数不是指定类的实例化对象则无法解析。
"o!"、"O!"与字符串用法相同。
#### 7.6.2.7 资源:r
如果参数为资源则可以通过"r"获取其zval的地址,但是无法直接解析到zend_resource的地址,与对象相同。
```c
zval *res;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "r", &res) == FAILURE){
...
}
```
"r!"与字符串用法相同。
#### 7.6.2.8 类:C
如果参数是一个类则可以通过"C"解析出zend_class_entry地址:`function my_func(stdClass){...}`,这里有个地方比较特殊,解析到的变量可以设定为一个类,这种情况下解析时将会找到的类与指定的类之间的父子关系,只有存在父子关系才能解析,如果只是想根据参数获取类型的zend_class_entry地址,记得将解析到的地址初始化为NULL,否则将会不可预料的错误。
```c
zend_class_entry *ce = NULL; //初始为NULL
if(zend_parse_parameters(ZEND_NUM_ARGS(), "C", &ce) == FAILURE){
RETURN_FALSE;
}
```
#### 7.6.2.9 callable:f
callable指函数或成员方法,如果参数是函数名称字符串、array(对象/类,成员方法),则可以通过"f"标识符解析出`zend_fcall_info`结构,这个结构是调用函数、成员方法时的唯一输入。
```c
zend_fcall_info callable; //注意,这两个结构不能是指针
zend_fcall_info_cache call_cache;
if(zend_parse_parameters(ZEND_NUM_ARGS(), "f", &callable, &call_cache) == FAILURE){
RETURN_FALSE;
}
```
函数调用:
```php
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`的用意是简化函数调用的操作,否则需要我们自己去查找函数、检查是否可被调用等工作,关于这个结构稍后介绍函数调用时再作详细说明。
#### 7.6.2.10 任意类型:z
"z"表示按参数实际类型解析,比如参数为字符串就解析为字符串,参数为数组就解析为数组,这种实际就是将zend_execute_data上的参数地址拷贝到目的变量了,没有做任何转化。
"z!"与字符串用法相同。
#### 7.6.2.11 其它标识符
除了上面介绍的这些解析符号以外,还有几个有特殊用法的标识符:"|"、"+"、"*",它们并不是用来表示某种数据类型的。
* __|:__ 表示此后的参数为可选参数,可以不传,比如解析规则为:"al|b",则可以传2个或3个参数,如果是:"alb",则必须传3个,否则将报错;
* __+/*:__ 用于可变参数,注意这里与PHP函数...的用法不太一样,PHP中可以把函数最后一个参数前加...,表示调用时可以传多个参数,这些参数都会插入...参数的数组中,"*/+"也表示这个参数是可变的,但内核中只能接收一个值,即使传了多个后面那些也解析不到,"*"、"+"的区别在于"*"表示可以不传可变参数,而"+"表示可变参数至少有一个。
';