10.6 函数
通过扩展可将C语言实现的函数提供给PHP脚本使用,如同大量PHP内置函数一样,这些函数也称为内部函数,它们无需经历PHP用户函数的编译过程,在执行时也无需每个指令都调用一次C语言编写的handler函数,而是直接执行编译好的机器指令,效率更高,且还能拥有更高的控制权限,能完成PHP用户函数无法实现的功能。
PHP中,函数通过zend_function联合体表示,PHP用户自定义函数使用op_array,内部函数使用internal_function,两者使用相同的头部信息:函数类型、参数信息、函数名等。


handler就是内部函数的指针。
10.6.1 内部函数注册
PHP用户函数和内部函数都会注册到EG(function_table)中,函数被调用时都会根据函数名从该符号表中查找。
通过扩展提供一个内部函数的步骤:
1.定义函数
可通过PHP_FUNCTION()或ZEND_FUNCTION()宏来完成函数的标准声明,该宏为函数名加上了zif_前缀,gdb调试时要加上该前缀。


如要在扩展中定义my_func():

2.注册函数
扩展需要为每个内部函数生成一个zend_function_entry结构,然后将所有要注册的内部函数的结构数组提供给扩展zend_module_entry->functions即可,注册扩展时就会自动向EG(function_table)注册。

zend_function_entry结构可通过PHP_FE()或ZEND_FE()生成,然后把所有zend_function_entry放到一个数组中,这个数组作为全局变量分配即可:

然后将zend_module_entry->functions设为mytest_functions即可:

注册扩展时,php_load_extension()会调用zend_register_module_ex()完成内部函数的注册。
内部函数定义完成并编译安装后,可在PHP脚本中调用这个函数来测试能否正常工作。
10.6.2 函数参数解析
PHP函数参数的实现中,PHP用户自定义函数在编译时会为每个参数创建一个zend_arg_info结构,这个结构记录参数名、是否引用传参、是否为可变参数(可变参数在PHP 5.5及以下,使用方式是在函数中调用func_get_args获取参数;在PHP 5.6及以上,使用方式类似这样function func(...$args),其中args是可变参数数组,只能放在最后一个参数位置)等信息,在存储上函数参数与函数内局部变量没有区别,也分配在zend_execute_data上;调用函数时按参数次序,依次将参数值从函数调用空间传递到被调函数的zend_execute_data,函数内部像访问局部变量一样通过存储位置访问参数。
内部函数是C语言定义的函数,其局部变量是C语言中的变量,不会分配在zend_execute_data上,但其参数会分配在zend_execute_data上,即内部函数的zend_execute_data里只有参数,没有其他变量。内部函数调用时,参数从PHP用户空间传到内部函数中,我们扩展中提供的内部函数里,可通过PHP提供的一个方法将zend_execute_data上的参数解析到指定变量上:

1.num_args:函数调用时实际传递的参数数量,通过ZEND_NUM_ARGS()获取到,即zend_execute_data->This.us.num_args。
2.type_spec:参数解析规则,是一个字符串,用来标识参数的类型,如"la"表示两个参数的类型分别为整型和数组。
3.之后是一个可变参数,指定解析到的变量,type_spec指定要解析到的变量的类型,可变参数指定要解析到的变量的地址。type_spec参数是一个字符串,如果有两个参数,那么字符串中就会规定两个参数的解析方式(解析方式见下面),此时应有两个可变参数来接收两个参数值。
被调函数执行前,参数值已经在zend_execute_data上了,所以解析的过程就是从zend_execute_data上获取参数,然后将参数值的地址赋值给目标变量(除了整型、浮点型、布尔型,它们直接将值复制到目标变量)。如下例:

my_func函数的第一个参数类型为数组,解析后arr为zend_execute_data上第一个参数的zval的地址,如图10-4:

zend_parse_paramters()调用了zend_parse_va_args(),zend_parse_va_args()中首先做了简单的检查,如检查必传参数数量,然后依次获取zend_execute_data中存储的参数,根据不同类型进行相应的解析。
参数解析时,如发现实参类型与要求的类型不符,则会按规则进行类型转换。
以上获取参数的方式是PHP 5.x中的方式,PHP 7中除了支持该方式,还提供了另一种更快的解析方式。新方式需要通过两个宏将各个参数的解析规则包裹起来:

ZEND_PARSE_PARAMETERS_START宏的第一个参数是必传参数数量,第二个参数是最多可传参数数量。以上两个宏之间按参数的顺序定义解析规则。两种不同的参数解析方式下,解析各类型时的区别:
1.整型:l、L
通过l、L标识的参数为整型,解析到的变量类型必须是zend_long,如果输入的参数不是整型,则按规则将其转为整型。例如:

如果在标识符后加上!,表示允许该参数传NULL,此时需要再提供一个zend_bool变量的地址参数,该变量为1时,表示传入的该参数为NULL,然后将zend_long变量值设为0,该zend_bool变量的作用是区分用户传的是NULL还是0,因为这两种情况下zend_long变量值都会被设为0。

各标识符的解析过程在zend_parse_arg_impl()中。
l与L的区别在于,当实参不是整型且转为整型后超过了整型的表示范围(int64,PHP不支持无符号整型,因此64位整型的最大值为0x7FFFFFFFFFFFFFFF)时,L会将值调整为整型的最大值或最小值,而l将报错。
以上是旧的解析方式,对于整型,新的解析方式通过以下宏完成:
(1)Z_PARAM_LONG(dest)、Z_PARAM_LONG_EX(dest, is_null, check_null, separate):相当于l。dest参数为zend_long;check_null参数取决于!;当调用函数时的参数为NULL时,将设置is_null参数;separate参数用于指定参数的value是否分离,当该值为1时,参数类型为string、array的将复制value。
(2)Z_PARAM_STRICT_LONG(dest)、Z_PARAM_STRICT_LONG_EX(dest, is_null, check_null, separate):相当于L。
新旧两种解析方式最终都是通过zend_parse_arg_long()处理的,只是获取解析到的变量的方式不同。
2.布尔型:b
b标识符表示将参数解析为布尔型,解析到的变量必须是zend_bool。b!的用法与整型的相同,用于判断传参是否为NULL:

以上是旧的解析方式,对于布尔型,新的解析方式通过宏Z_PARAM_BOOL(dest)或Z_PARAM_BOOL_EX(dest, is_null, check_null, separate)完成。
3.浮点型:d
d标识符表示将参数解析为布尔型,解析到的变量必须是double。d!与整型、布尔型的用法相同。

以上是旧的解析方式,对于浮点型,新的解析方式通过宏Z_PARAM_DOUBLE(dest)或Z_PARAM_DOUBLE_EX(dest, is_null, check_null, separate)完成。
4.字符串:s、S、p、P
字符串可被解析为char *或zend_string *。s表示将参数解析为char *,此时需要额外提供一个size_t类型的变量用于获取字符串长度;S表示将参数解析为zend_string *。s!、S!时,不需要提供额外的zend_bool变量的地址,如果参数为NULL,则解析到的变量值将被设为NULL。p、P用于解析路径,但路径与普通字符串没什么区别,尚不清楚为什么单独用p、P来解析路径。

以上是旧的解析方式,对于字符串,新的解析方式通过以下宏完成:
(1)Z_PARAM_STRING(dest, dest_len)、Z_PARAM_STRING_EX(dest, dest_len, check_null, separate):相当于s。如果separate为1,且参数为普通字符串(而非内部字符串),那么会把实参字符串深拷贝一份作为参数值。
(2)Z_PARAM_STR(dest)、Z_PARAM_STR_EX(dest, check_null, separate):相当于S。
(3)Z_PARAM_PATH(dest, dest_len)、Z_PARAM_PATH_EX(dest, dest_len, check_null, separate):相当于p。
(4)Z_PARAM_PATH_STR(dest)、Z_PARAM_PATH_STR_EX(dest, check_null, separate):相当于P。
具体解析函数为zend_parse_arg_str()、zend_parse_arg_string()。
5.数组:a、A、h、H
a、A将数组解析到zval *;h、H将数组解析到HashTable *。四个标识符后都可加!,但不需要提供额外地址,当实参为NULL时,则对应解析到的变量值为NULL。a和A的区别在于,当实参为对象时,A会将对象解析到zval,而a会报错。h和H的区别在于,当实参为对象时,H会调用对象的get_properties获取属性数组,然后将这个数组的地址赋值给解析到的变量,而h会报错。

上图是旧的解析方式,对于数组,新的解析方式通过以下宏完成:
(1)Z_PARAM_ARRAY(dest)、Z_PARAM_ARRAY_EX(dest, check_null, separate):相当于a。当separate为1,且参数为普通数组时,会深拷贝数组。
(2)Z_PARAM_ARRAY_OR_OBJECT(dest, check_null, separate)、Z_PARAM_ARRAY_OR_OBJECT_EX(dest, check_null, separate):相当于A。Z_PARAM_ARRAY_OR_OBJECT宏虽然有三个参数,但后面两个没有用。
(3)Z_PARAM_ARRAY_HT(dest)、Z_PARAM_ARRAY_HT_EX(dest, check_null, separate):相当于h。
(4)Z_PARAM_ARRAY_OR_OBJECT_HT(dest)、Z_PARAM_ARRAY_OR_OBJECT_HT_EX(dest, check_null, separate):相当于H。
具体解析函数为zend_parse_arg_array()、zend_parse_arg_array_ht()。
6.对象:o、O
通过o、O标识符可将实参解析为对象,解析到的变量类型为zval的地址。O会检查实参类型,即指定参数类型的情况function my_func(MyClass $obj),如类型不符则无法解析。也可在标识符后加上叹号:o!、O!。

以上是旧的解析方式,对于对象,新的解析方式通过以下宏完成:
(1)Z_PARAM_OBJECT(dest)、Z_PARAM_OBJECT_EX(dest, check_null, separate):相当于o。
(2)Z_PARAM_OBJECT_OF_CLASS(dest, _ce)、Z_PARAM_OBJECT_OF_CLASS_EX(dest, _ce, check_null, separate):相当于O。
7.资源:r
通过r标识符可将实参解析为资源,解析到的变量类型为zval的地址。也可在标识符后加上叹号:r!。

以上是旧的解析方式,对于资源,新的解析方式通过宏Z_PARAM_RESOURCE(dest)、Z_PARAM_RESOURCE_EX(dest, check_null, separate)解析。实参类型只能为资源,不能将其他类型转换为资源。
8.类:C
当实参类型为类名字符串时,即function my_class(stdClass),C可解析出zend_class_entry的地址。解析到的变量可以为类类型,此时变量类型和实参类型必须存在父子关系才能解析。如果只想根据实参获取其类型的zend_class_entry地址,要将解析到的地址初始化为NULL,否则会出现不可预料的错误。


以上是旧的解析方式,对于类,新的解析方式通过宏Z_PARAM_CLASS(dest)或Z_PARAM_CLASS_EX(dest, check_null, separate)完成。
9.回调类型(callable):f
callable指函数或方法,如果参数是函数名字符串、array(obj/class, method),则可通过f标识符解析出zend_fcall_info结构。

调用函数时,可传以下参数:

解析出的zend_fcall_info可直接用于函数调用,函数查找、检查是否可调用等工作已被完成。
以上是旧的解析方式,对于回调类型,新的解析方式通过宏Z_PARAM_FUNC(dest_fci, dest_fcc)或Z_PARAM_FUNC_EX(dest_fci, dest_fcc, check_null, separate)完成。
10.任意类型:z
z表示按参数实际类型解析,就是将zend_execute_data上的参数地址复制到目的标量了。z!与字符串用法相同。
新的解析方式通过Z_PARAM_ZVAL(dest)或Z_PARAM_ZVAL_EX(dest, check_null, separate)宏完成。
11.其他标识符
(1)|:表示此后的参数为可选参数,新的解析方式中,可用Z_PARAM_OPTIONAL宏替代:


(2)+、*:用于可变参数,但*表示可以不传可变参数,+表示可变参数至少有一个。可变参数将被解析到zval数组,可通过一个整型参数获取参数数量。

以上是旧的解析方式,对于可变参数,新的解析方式通过宏Z_PARAM_VARIADIC(spec, dest, dest_num)完成,其中spec为*或+;dest为参数数组的地址;dest_num为参数数量。
10.6.3 引用传参
用户PHP自定义函数中,参数通过zend_arg_info结构保存信息,而内部函数使用zend_internal_arg_info结构保存参数信息:

该结构与zend_arg_info结构相比,不同之处在于name、class_name属性的类型,zend_arg_info的这两个属性的类型为zend_string。
如果函数需要使用引用类型的参数或返回引用,则需要使用新的方式定义参数,即提供一个zend_internal_arg_info结构数组,每个结构表示一个参数,这需要通过宏ZEND_BEGIN_ARG_INFO()或ZEND_BEGIN_ARG_INFO_EX()作为开头,ZEND_END_ARG_INFO()宏作为结尾,这对宏其实就是创建了一个参数数组。

上图中宏的参数:
1.name:参数数组名,注册函数PHP_PE(function, arg_info)会用到。
2.unused:保留值,暂时无用。
3.return_reference:返回值是否为引用。
4.required_num_args:required参数数量。
用法:

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


例如:

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


展开后:

第一个数组元素用于记录必传参数的数量以及返回值是否为引用。之后使用PHP_FE宏生成函数的zend_function_entry结构,放到扩展的zend_function_entry:

引用参数通过zend_parse_parameter()解析时只能使用z解析,不能将其解析为zend_value,否则引用将失效:

10.6.4 函数返回值
函数的返回值地址在调用内部函数时作为参数传入,即return_value指针参数。注意,需要考虑是否要增加返回值value的引用计数,例如:

上图中my_func_1函数接收一个数组作为参数,然后直接返回该数组,数组会多一个来自返回值的引用,因此需要增加引用计数。
虽然可以直接修改return_value,但并不建议这样做,PHP在zend_API.h中定义了很多专门用于设置返回值的宏:




10.6.5 函数调用
本节介绍PHP提供的用于函数调用的API,即call_user_function(),注意,它是C API,与PHP脚本中常使用的call_user_func函数不同:

参数含义:
1.function_table:函数符号表,如果要调用的是普通函数,该参数就是EG(function_table);如果要调用的是类方法,该参数就是zend_class_entry.function_table。
2.object:调用的方法对应的对象。
3.function_name:调用的函数名。
4.retval_ptr:函数返回值地址。
5.param_count:实际传参数量。
6.params:参数数组。
从下例看一下如何在内部函数中调用PHP用户空间的函数:
1.在PHP中定义一个函数:

2.在扩展的内部函数my_func中调用它:

3.调用扩展的内部函数my_func:

call_user_function()不仅能调用PHP脚本中定义的函数,也能调用内核或其它扩展注册的函数,如array_merge():

调用上图函数:

上例中,没有增加两个数组参数的引用计数,实际上时call_user_function()替我们完成了这一操作:

call_user_function()将我们提供的参数组装为zend_fcall_info结构,然后调用zend_call_function。zend_parse_parameters()的解析符f也能将输入的函数名解析为一个zend_fcall_info结构,或者自己创建一个zend_fcall_info结构,都可以完成函数的调用。

由上图,zend_call_function()会在调用函数前遍历参数,如果参数的类型用到了引用计数则增加对应参数的引用计数。
10.7 zval的操作
扩展中经常会用到各种类型的zval,虽然可以自己操作zval,但这不是一个好习惯,因为zval有很多标识,如果自己去管理非常烦琐且易出错,因此在扩展中操作zval时要尽可能地使用内核提供的宏或方法。
10.7.1 zval的创建及获取
PHP 7将变量的引用计数转移到了具体的value上,zval更多的是作为统一的传输格式,很多情况只是临时使用,如函数调用时传参,函数需要的是zval携带的zend_value,函数从zval取得zend_value后就不再关心zval了。分配zval后需要设置其类型、value等信息,PHP提供了ZVAL_XXX()系列宏来完成这些操作,这些宏的第一个参数z均为要设置的zval指针,后面为要设置的zend_value。内核还提供了Z_XXX(zval)、Z_XXX_P(zval_p)系列的宏,用于获取zval的value。
zval的类型可通过Z_TYPE(zval)、Z_TYPE_P(zval_p)宏获取,实际上获取的就是zval.ul.v.type:

1.UNDEF及NULL
通过ZVAL_UNDEF(z)将zval设置为UNDEF;通过ZVAL_NULL(z)将zval设置为NULL。判断zval是否为UNDEF、NULL的宏:


2.布尔型
布尔型有两类,一类是老版本的方式,布尔型为IS_BOOL,然后根据0、1区分true、false,现已不再使用;另一类是将true、false分别定义为一种类型,即IS_TRUE、IS_FALSE。内核提供了三个宏来定义布尔型:
(1)ZVAL_BOOL(z, b),b为IS_TRUE或IS_FALSE。
(2)ZVAL_FALSE(z),将z设置为false。
(3)ZVAL_TRUE(z),将z设置为true。
如果想判断一个zval是否是浮点型,直接使用Z_TYPE(zval)、Z_TYPE_P(zval_p)获取其类型进行判断即可。
3.整型、浮点型
整型、浮点型分别通过ZVAL_LONG(z, l)、ZVAL_DOUBLE(z, d)设置,l、d值的类型分别为zend_long、double。整型值可通过Z_LVAL(zval)、Z_LVAL_P(zval_p)取到;浮点型值可通过Z_DVAL(zval)、Z_DVAL_P(zval_p)取到。
4.字符串
字符串在内核中分为char、zend_string两类,且还有内部字符串。一个设置字符串的宏是ZVAL_STR(z, s),s可以是zend_string类型的地址,该宏不会将s的字符串复制一份,而是将字符串值的地址设置到新zval,s的引用计数不会增加,如需增加,则需要手动增加;s也可以是内部字符串,此时会设置zval的type类型:

另一个类似的宏ZVAL_NEW_STR(z, s),它除了会将内部字符串当作普通字符串对待外,与ZVAL_STR(z, s)一样。
还有另一个会增加字符串引用计数的宏ZVAL_STR_COPY(z, s),它与ZVAL_STR(z, s)相比,会根据s的类型决定是否增加引用计数(如果不是内部字符串就增加,内部字符串不需要增加引用计数,因为它的生命周期贯穿始终,是PHP内核的永久缓存):

可通过Z_STR(zval)、Z_STR_P(zval_p)获取字符串zend_string的地址;Z_STRVAL(zval)、Z_STRVAL_P(zval_p)获取char字符串,即zend_string的val成员;Z_STRLEN(zval)、Z_STRLEN(P(zval_p)获取字符串长度,即zend_string的len成员;Z_STRHASH(zval)、Z_STRHASH_P(zval_p)获取字符串的哈希值,即zend_string的h成员。
5.数组
通过ZVAL_ARR(z, a)可将zend_array的地址a赋给z,该宏不会增加a的引用计数;也可通过ZVAL_NEW_ARR(z)分配一个zend_array给z,但只是分配了zend_array内存,还没有进行数组初始化,数组还不能使用:

数组地址可通过宏Z_ARR(zval)、Z_ARR_P(zval_p)、Z_ARRVAL(zval)、Z_ARRVAL_P(zval_p)获得。
6.对象
对象可通过ZVAL_OBJ(z, o)宏设置,该宏不会增加o的引用计数。可通过宏Z_OBJ(zval)、Z_OBJ_P(zval_p)取到zend_object的地址。还有以下宏来获取对象的其他信息:

7.资源
资源可通过ZVAL_RES(z, r)宏设置,r为zend_resource地址,该宏不会增加r的引用计数。可通过Z_RES(zval)、Z_RES_P(zval_p)宏获取zend_resource地址;通过Z_RES_HANDLE(zval)、Z_RES_HANDLE_P(zval_p)获取zend_resource的handle成员。
8.引用
可通过ZVAL_REF(z, r)宏将zend_reference的地址r设置到z,该宏不会增加r的引用计数;也可通过ZVAL_NEW_REF(z, r)创建一个引用,r为要引用的值:


可通过Z_REF(zval)、Z_REF_P(zval_p)获取zend_reference地址;通过Z_REFVAL(zval)、Z_REFVAL_P(zval_p)获取引用的实际值。
除了以上PHP变量相关的宏,还有操作内核自己使用的类型的宏,因为zval是一个通用型结构,可用它保存任意类型,如类、函数等:



&spm=1001.2101.3001.5002&articleId=148526041&d=1&t=3&u=1abc2398606940c8816beb6dbbecb004)
7904

被折叠的 条评论
为什么被折叠?



