PHP7内核剖析 学习笔记 第十章 扩展开发(2)

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.整型:lL

通过l、L标识的参数为整型,解析到的变量类型必须是zend_long,如果输入的参数不是整型,则按规则将其转为整型。例如:
在这里插入图片描述
如果在标识符后加上!,表示允许该参数传NULL,此时需要再提供一个zend_bool变量的地址参数,该变量为1时,表示传入的该参数为NULL,然后将zend_long变量值设为0,该zend_bool变量的作用是区分用户传的是NULL还是0,因为这两种情况下zend_long变量值都会被设为0。
在这里插入图片描述
各标识符的解析过程在zend_parse_arg_impl()中。

lL的区别在于,当实参不是整型且转为整型后超过了整型的表示范围(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.字符串:sSpP

字符串可被解析为char *zend_string *s表示将参数解析为char *,此时需要额外提供一个size_t类型的变量用于获取字符串长度;S表示将参数解析为zend_string *s!S!时,不需要提供额外的zend_bool变量的地址,如果参数为NULL,则解析到的变量值将被设为NULL。pP用于解析路径,但路径与普通字符串没什么区别,尚不清楚为什么单独用pP来解析路径。
在这里插入图片描述
以上是旧的解析方式,对于字符串,新的解析方式通过以下宏完成:
(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.数组:aAhH

aA将数组解析到zval *hH将数组解析到HashTable *。四个标识符后都可加!,但不需要提供额外地址,当实参为NULL时,则对应解析到的变量值为NULL。aA的区别在于,当实参为对象时,A会将对象解析到zval,而a会报错。hH的区别在于,当实参为对象时,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.对象:oO

通过oO标识符可将实参解析为对象,解析到的变量类型为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是一个通用型结构,可用它保存任意类型,如类、函数等:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值