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

10.7.2 变量复制

扩展中我们经常遇到将一个变量复制到另一个变量的场景,这可通过宏ZVAL_COPY(z, v)完成,该宏会将v复制到z表示的zval,且会增加对应value的引用计数。此外,ZVAL_COPY_VALUE(z, v)宏也可完成变量复制,但不会增加value的引用计数。

由以上宏展开可见,复制时不会发生硬拷贝,只是将value的指针复制到了目标zval。这两个宏还会将变量的类型赋值给目标zval。具体value的复制在ZVAL_COPY_VALUE_EX()宏中完成:

上图中,并没有看到具体value的复制,实际Z_COUNTED_P(z) = gc就已经完成了复制,因为可通过zend_value的任意成员获取到zend_value的内存地址。这里完成复制后,可以使用zend_value->counted成员获取到值。

10.7.3 引用计数

扩展中操作PHP用户空间的变量时,要考虑是否需要引用计数相关的操作,如下例:

如果函数test用内部函数实现,该函数接受了一个PHP用户空间传入的数组参数,然后又返回并赋值给PHP用户空间的另一个变量,此时就需要增加传入数组的refcount。上图中函数test翻译成内部函数:

如果内部函数中操作的是与PHP用户空间相关的变量,那么就需要增加其引用计数。PHP中常见的会操作引用计数的情况:

1.变量赋值:此时会增加引用计数。

2.数组操作:将一个变量插入数组,会增加该变量引用计数;将一个变量从数组中删除,会减少该变量引用计数。

3.函数调用:传参可看成普通的变量复制,会增加该变量引用计数;函数返回会销毁函数空间内的变量,会减少该变量引用计数。这两个过程由内核完成,不需扩展自己处理。

4.成员属性:把变量赋值给成员属性时会增加该变量的引用计数。

PHP定义了以下宏用于引用计数相关操作:

上图中宏操作的类型都是zval或zval *,可用以下宏直接操作value的引用计数:

关于引用计数还有以下常用宏:

10.7.4 字符串操作

zend_string是PHP封装的一个用于表示字符串的结构,以下是操作zend_string的宏和函数:

除上图这些,还有很多其他API定义在zend_operators.h中,如字符串大小写转换、字符串比较等。

10.7.5 数组操作

10.7.5.1 创建数组

创建HashTable分两步:首先分配zend_array内存,可通过ZVAL_NEW_ARR()宏分配,也可以自己直接分配;然后初始化数组,可通过zend_hash_init()宏完成。

上图参数:

1.ht:类型为数组地址HashTable *,如果内部使用可直接通过emalloc分配。

2.nSize:初始化大小,该值会被对齐到2的幂,最小为8。

3.pHashFunction:无用,设为NULL即可。

4.pDestructor:删除数组元素时会调用该函数对元素进行处理,比如删除元素时,调用该函数处理其引用计数以及其他清理工作。

5.persistent:是否持久化。持久化数组只能在内部使用,不能传给PHP用户空间,如果是持久化,会调用malloc分配内存,同时数组的key、arrData的空间也是通过malloc分配的。如果此处用持久化,那么ht在分配的时候也应使用持久化的方式:

上图中,zend_hash_init的最后一个参数不能是使用持久化,因为ZVAL_NEW_ARR()分配的zend_array是非持久化的,可使用ZVAL_NEW_PERSISTENT_ARR(z)代替ZVAL_NEW_ARR。

10.7.5.2 插入、更新元素

插入、更新元素分三种情况:key为zend_string、key为char字符串、key为数值索引。以下是相关的宏和函数。

1.key为zend_string

下图中的pData是zval地址:

2.key为char字符串

3.key为数值索引

10.7.5.3 查找元素

用来查找元素的宏根据key的类型也分为三种:

10.7.5.4 删除元素

10.7.5.5 遍历

在扩展中可通过以下方式遍历数组:

遍历过程会把数组元素赋值给val。还有以下常用的遍历数组的宏:

10.7.5.6 其他操作

zend_hash.c文件中还有其他的数组操作,如:

zend_hash_sort宏的compare_func参数的类型为typedef int (*compare_func_t)(const void *, const void),该参数用来自己定义比较函数,比较函数的参数类型为Bucket;renumber参数表示是否更改键值,如果为1则会在排序后重新生成各元素的h。PHP中的sort、rsort、ksort等都是基于该函数实现的。

10.7.5.7 销毁

数组销毁时会释放所有key、value,如果数组没有自定义用来销毁value的pDestructor,则默认会调用i_zval_ptr_dtor销毁value。

10.8 常量

PHP提供了很多用于注册不同类型常量的宏,这些宏定义在zend_constants.h头文件中:

除了上面这些,还有REGISTER_NS_XXX系列宏用于带namespace的常量注册。如果要注册上图中没有的类型的常量,如果数组类型常量,可通过zend_register_constant(zend_constant *c)注册。

扩展中会在PHP_MINIT_FUNCTION()中注册常量,例如:

编译后,在PHP代码中就可以使用常量MY_CONS_1,其内容是字符串"this is a constant"

扩展中如果要用到其它扩展或内核定义的常量,通过以下函数获取:

10.9 面向对象

通过扩展实现的类不需要编译,有更高的性能。

10.9.1 内部类注册

类似函数注册,函数会注册到EG(function_table),而类会注册到EG(class_table)符号表中。注册过程首先是为类创建一个zend_class_entry结构,然后把这个结构插入EG(class_table),这个过程不需要手动操作,PHP提供了宏来完成zend_class_entry的初始化与注册。通常会把内部类的注册放到module startup阶段,即定义在PHP_MINIT_FUNCTION()中,如:

上图定义了一个内部类,类名为MyClass,这个类没有任何属性和方法。安装扩展后可在PHP脚本中实例化该类:

类的注册是通过zend_register_internal_class()完成的,注册时传入的zend_class_entry并不是最终插入EG(class_table)的结构,因此参数可分配在栈上。注册完成后,zend_register_internal_class()将返回实际的zend_class_entry地址,扩展可将这个地址保存下来,用到类的地方直接使用,不需再通过EG(class_table)查找:

内核还提供了以下两个宏用于初始化zend_class_entry:

10.9.2 成员属性

成员属性的操作接口在zend_API.h中,主要包括成员属性的声明、修改、获取操作。

1.声明成员属性

PHP提供了各种标量类型的成员属性声明接口:

以上API最后一个参数都是access_type,该参数用来指定属性权限以及是否为静态属性:

通常属性的声明与类的定义放一起,即在module startup中完成:

上图内容重新编译扩展后可在PHP脚本中执行:

注册属性时,属性名的长度不包括C风格字符串最后的空字符,可通过ZEND_STRL宏计算属性名长度:

内部类声明的属性不能是数组、对象、资源,否则会报错,可以把这些类型的属性值定义为NULL,然后运行时再修改为其他类型即可。

2.修改成员属性

3.获取成员属性

10.9.3 成员方法

1.定义

内核提供了两个宏用于方法的定义:

定义时指定类名(如下例MyClass)、方法名(如下例get)即可,下图中花括号内即为方法的C语言实现:

2.注册

我们可将所有成员方法保存到一个数组中,然后在使用INIT_CLASS_ENTRY宏初始化类时,作为第3个参数传入方法数组即可。方法数组中的成员可通过PHP_ME(classname, name, arg_info, flags)宏定义,其中flags用于标识该方法的权限、final、abstract、static等信息:

10.9.4 常量

1.声明常量

2.获取常量

可通过以下接口获取常量,其name参数需包含::,如self::常量名parent::常量名获取基类中的同名常量、static::常量名获取运行时的实际调用的类的常量(可能是基类,也可能是派生类,具体看变量类型):

10.9.5 类的实例化

扩展中可通过object_init_ex()创建一个对象:

_object_init_ex()最后将调用类的create_object方法创建对象,我们也可以自定义类的create_object方法,具体实现可参考zend_objects_new方法,它是默认的create_object方法:

如果自定义了create_object方法,则需要考虑是否定义销毁对象的handler:zend_object->handlers->free_obj,用来回收create_object方法中分配的内存等资源。

10.10 资源

socket连接、文件句柄等是资源类型,资源类型只能通过扩展、内核定义,用户空间中只能使用资源。PHP中任何数据都可定义为资源类型。

注册新的资源类型时,PHP内核不关心具体数据是什么,它只分配给资源一个唯一编号作为区分资源类型的表示,剩下的事情需要扩展自己控制。资源结构如下:

上图中,handle属性是一次请求期间分配的所有资源序号,按资源的创建顺序分配;type为资源类型;ptr是资源具体的数据。

1.注册资源类型

注册时只是向内核申请了一个资源类型编号,同时告诉该资源的销毁函数句柄。注册资源通过zend_register_list_destructors_ex()完成:

ld是资源的销毁函数;pld是销毁资源类型的回调函数;type_name是资源类型名;module_number是模块编号。注册成功后返回内核分配的资源类型值,该值需保存下来供后续获取、创建资源等操作使用。下面以读写为例,定义文件指针为一种资源类型,即实现C语言中fopen、fwrite操作。

首先定义一个结构用于保存文件指针,然后把该结构作为一种资源:

然后注册资源,通常在module startup阶段注册:

之后就可以创建并使用这种类型的资源了。zend_register_list_destructors_ex()会为资源类型创建一个zend_rsrc_list_dtors_entry结构:

内核把所有资源类型都保存到了哈希表list_destructors中,它是一个全局变量,注册资源后可通过gdb查看到我们注册的资源:

2.创建资源

可通过zend_register_resource()申请一个资源类型的变量,即一个zend_resource类型的变量:

上图中,rsrc_pointer参数是资源的数据指针;rsrc_type参数是资源类型。资源申请成功后会返回zend_resource的地址。分配的所有资源变量保存在EG(regular_list)哈希表中,请求结束后会清理这个哈希表。

接下来我们实现打开文件的函数,类似C语言中的fopen函数。要实现的函数会返回一个资源类型,返回后可通过这个资源读写文件:

编译扩展后可在PHP中调用my_fopen函数:

执行后将输出:

之后我们实现类似fwrite操作的my_fwrite函数,参数之一是my_fopen()返回的资源:

编译扩展后可在PHP中调用my_fwrite:

从上例可见,通过资源类型可以灵活地存储任意数据。PHP扩展中,很多RPC客户端也是通过资源类型来存储socket连接的。ext目录下有很多资源的使用范例。

3.销毁资源

注册资源类型时可提供一个析构函数,当资源销毁时,资源管理器会调用该函数进行清理,这个函数类型如下:

我们可提供一个myfile_dtor(),用于释放资源(关闭文件描述符):

4.持久化资源

普通资源的生命周期在request结束时也随之结束,持久化资源可以使资源在request结束时仍然保留,下次请求时还可以继续使用。持久化资源通过EG(persistent_list)保存,直接将资源插入这个哈希表即可。持久化资源在module shutdown阶段才被释放。

相关推荐
猹叉叉(学习版)2 小时前
【ASP.NET CORE】 11. SignalR
笔记·后端·c#·asp.net·.netcore
ok_hahaha2 小时前
java从头开始-苍穹外卖-day11-数据统计与展示
java
MyY_DO2 小时前
继承+代码复用使用方法说人话
java·开发语言
qq5680180762 小时前
一个基于Spring Boot的简单网吧管理系统
java·spring boot·后端
前端小雪的博客.2 小时前
Java for 循环详解:从基础语法到实战案例(新手友好版)
java·java基础·for循环·循环结构
断手当码农2 小时前
Java算法题常见的20种输入模板(ACM / LeetCode 通用)
java
hashiqimiya2 小时前
spring报错
java·后端·spring
yaoxin5211232 小时前
352. Java IO API - Java 文件操作:java.io.File 与 java.nio.file 功能对比 - 4
java·python·nio
for_ever_love__2 小时前
Objective-C 学习 NSString 和 NSMutableString的基本功能详解
学习·ios·objective-c