perlclassguts - feature 'class' 和类语法的工作原理的内部机制
本文档详细介绍了 perl 解释器实现 feature 'class' 语法和整体行为的方式。它不是关于如何使用该功能的最终用户指南。有关该内容,请参阅 perlclass。
假定读者总体上熟悉 perl 解释器内部机制。有关这些详细信息的更全面概述,另请参阅 perlguts。
类从根本上来说是一个包,并且以 HV 的形式存在于符号表中,其辅助结构与非类包完全相同。它与非类包的区别在于 HvSTASH_IS_CLASS() 宏将在其上返回 true。
与它作为类的相关附加信息存储在附加到 stash 的 struct xpvhv_aux 结构中,在以下字段中
HV *xhv_class_superclass;
CV *xhv_class_initfields_cv;
AV *xhv_class_adjust_blocks;
PADNAMELIST *xhv_class_fields;
PADOFFSET xhv_class_next_fieldix;
HV *xhv_class_param_map;
对于没有超类的类,xhv_class_superclass 将为 NULL。如果已使用 :isa() 类属性设置了超类,它将直接指向父类的 stash。
xhv_class_initfields_cv 将包含一个 CV *,指向一个函数,该函数将作为此类或其任何子类的构造函数的一部分被调用。此 CV 负责为新实例初始化此类定义的所有字段。此 CV 将是一个匿名的真实函数 - 即,虽然它没有名称和 GV,但它不是一个 protosub,并且可以直接调用。
xhv_class_adjust_blocks 可能指向一个包含指向类上定义的每个 ADJUST 块的 CV 指针的 AV。如果该类有超类,此数组还将包含其父类的 CV 的重复指针。AV 在第一次向其推送元素时会延迟创建;如果没有一个,则它是有效的,并且在这种情况下此指针将为 NULL。
CV 直接存储,而不是通过 RV。每个 CV 将是一个匿名的真实函数。
xhv_class_fields 将指向一个包含 PADNAME 的 PADNAMELIST,每个 PADNAME 都是类的已定义字段之一。它们按声明顺序存储。但请注意,此数组的索引不一定等于每个字段的 fieldix,因为在子类的情况下,数组将从零开始,但如果其父类包含任何字段,则其第一个字段的索引将为非零。
有关如何表示各个字段的更多信息,请参见 "字段"。
xhv_class_next_fieldix 给出将分配给要添加到类的下一个字段的字段索引。它仅在编译时有用。
xhv_class_param_map 可能指向一个将字段 :param 属性名称映射到具有该名称的字段的字段索引的 HV。此映射从父类复制;除了自己的之外,每个类都将包含其所有父类的总和。
一个字段从本质上仍然是在作用域中声明的词法变量,并且存在于其对应 CV 的 PADNAMELIST 中。方法和其他类似方法的 CV 仍然可以像使用常规词法一样完全捕获它们。字段与其他类型的 pad 条目的区别在于,PadnameIsFIELD() 宏将对其返回 true。
与字段相关的附加信息存储在可通过 padname 上的 PadnameFIELDINFO() 宏访问的附加结构中。此结构具有以下字段
PADOFFSET fieldix;
HV *fieldstash;
OP *defop;
SV *paramname;
bool def_if_undef;
bool def_if_false;
fieldix 存储字段的“字段索引”;也就是说,存储此字段值的实例字段数组中的索引。请注意,数组中的第一个索引没有专门保留。类中的第一个字段将从字段索引 0 开始。
fieldstash 存储定义此字段的类的 stash 的指针。如果在同一作用域内定义了多个类,则有必要这样做;它用于消除每个类的字段的歧义。
{
class C1; field $x;
class C2; field $x;
}
defop 可能存储指向此字段的默认表达式 optree 的指针。默认表达式是可选的;此字段可能是 NULL。
paramname 可能指向包含给定给字段的 :param 名称属性的常规字符串 SV。如果没有,它将为 NULL。
如果使用 //= 或 ||= 运算符分别设置默认表达式,则 def_if_undef 和 def_if_false 之一将为 true。
方法从本质上仍然是 CV,并且具有与方法相同的基本表示形式。它具有 optree 和 pad,并通过其包含包的 stash 中的 GV 存储。通过 CvIsMETHOD() 宏将在其上返回 true,它与非方法 CV 区分开来。
(注意:此宏不应与以前称为 CvMETHOD() 的宏混淆。该宏与类系统无关,并且已重命名为 CvNOWARN_AMBIGUOUS() 以避免此混淆。)
当前没有需要存储有关方法 CV 的额外信息,因此结构不会添加任何新字段。
对象实例由一个全新的 SV 类型表示,其基本类型为 SVt_PVOBJ。这仍应被祝福到其类 stash 中,并以经典对象的通常方式包装在 RV 中。
由于这些是它们自己独特的容器类型,不同于哈希或数组,因此当被问及这些类型时,核心 builtin::reftype 函数会返回一个新值。该值为 "OBJECT"。
在内部,这样的对象是一个 SV 指针数组,其大小在创建时固定(因为编译后已知类中的字段数)。对象实例存储其中的最大字段索引(用于访问的基本错误检查)和存储各个字段值的固定大小的 SV 指针数组。
数组和哈希类型的字段直接将 AV 或 HV 指针存储到数组中;它们不是通过中间 RV 存储的。
上述数据结构由以下 API 函数提供支持。
void class_setup_stash(HV *stash);
在遇到 class 关键字时,由解析器调用。它将 stash 升级为类,并准备接收类特定项,如方法和字段。
void class_seal_stash(HV *stash);
在 class 块结束时,或对于单元类,在包含它的作用域中,由解析器调用。此函数执行各种最终化活动,这些活动在构造类的实例之前是必需的,但在了解类的所有成员信息之前无法完成。
必须在这两个函数调用之间对正在编译的类进行任何添加或修改。一旦类被密封,就不能再修改。
void class_add_field(HV *stash, PADNAME *pn);
由 pad.c 调用,作为在当前 pad 中定义新字段名称的一部分。请注意,此函数不会创建 pad 名称;这必须已经由 pad.c 完成。此 API 函数只是通知类,新的字段名称已创建,现在可供其使用。
void class_add_ADJUST(HV *stash, CV *cv);
解析器在解析和构造新 ADJUST 块的 CV 后调用。这将被添加到类存储的列表中。
void class_prepare_initfield_parse();
在解析字段变量的初始化表达式之前,由解析器调用。这利用挂起的 compcv 将所有字段初始化表达式组合到同一个 CV 中。
void class_set_field_defop(PADNAME *pn, OPCODE defmode, OP *defop);
在解析完字段的初始化表达式后,由解析器调用。设置默认表达式和应用模式。defmode 应为零,或根据默认模式为 OP_ORASSIGN 或 OP_DORASSIGN 之一。
#define padadd_FIELD
此标志常量告诉 pad_add_name_* 系列函数,应将新名称添加为字段。无需调用 class_add_field();这将自动完成。
void class_prepare_method_parse(CV *cv);
在调用 start_subparse() 之后但立即在执行任何其他操作之前由解析器调用。这会准备 PL_compcv 以解析方法;安排 CvIsMETHOD 测试为真,添加 $self 词法,以及可能需要的任何其他活动。
OP *class_wrap_method_body(OP *o);
在将方法体解析为 optree 但在将其包装到最终 CV 中之前,由解析器在方法体解析结束时调用。此函数将额外的操作插入到 optree 中以使方法正常工作。
#define SVt_PVOBJ
与 SvTYPE() 宏进行比较时使用的 SV 类型常量。
SSize_t ObjectMAXFIELD(sv);
一个函数式宏,用于获取可从 ObjectFIELDS 数组访问的最大有效字段索引。
SV **ObjectFIELDS(sv);
一个函数式宏,用于直接从对象实例中获取字段数组。字段可以通过其字段索引进行访问,从 0 到 ObjectMAXFIELD 给出的最大有效索引。
newUNOP_AUX(OP_METHSTART, ...);
OP_METHSTART 是一个 UNOP_AUX,它必须存在于方法 CV 的开头才能使其正常工作。这是由 class_wrap_method_body() 插入的,甚至出现在与签名参数检查或提取关联的任何 optree 片段之前。
此操作负责将 $self 的值移出参数列表,并将方法需要访问的任何字段变量绑定到 pad 中。AUX 向量将包含所需字段/pad 索引配对的详细信息。
此操作还对调用者值执行健全性检查。它检查它是否绝对是兼容类类型的对象引用。如果不是,则会引发异常。
如果 op_private 字段包含 OPpINITFIELDS 标志,则表示该操作开始特殊的 xhv_class_initfields_cv CV。在这种情况下,它还应从参数列表中获取第二个值,该值应为普通 HV 指针(直接,而不是通过 RV)。并将其绑定到第二个 pad 插槽,生成的 optree 将希望在那里找到它。
OP_INITFIELD 仅在实例的构造阶段作为 xhv_class_initfields_cv CV 的一部分被调用。这是组成实例的可变字段(包括 AV 和 HV)的各个 SV 实际分配到 ObjectFIELDS 数组中的时间。OPpINITFIELD_AV 和 OPpINITFIELD_HV 私有标志指示它是否正在创建 AV 或 HV;如果两者都没有设置,则会创建一个 SV。
如果操作具有 OPf_STACKED 标志,则它希望在堆栈上找到一个初始化值。对于 SV,这是数据堆栈中最上面的 SV。对于 AV 和 HV,它期望一个标记列表。
ADJUST Phasers在编译期间,ADJUST phaser 的解析方式与现有的 perl phaser(BEGIN 等)有根本性的不同
与采用常规途径不同,标记生成器识别出 ADJUST 关键字引入了 phaser 块。然后,解析器解析此块的主体,类似于解析(匿名)方法主体的方式,创建一个没有名称 GV 的 CV。然后,通过调用 class_add_ADJUST 将其直接插入类信息中,完全绕过符号表。
在编译期间,类和字段的属性的处理方式与子例程和词法变量上的现有 perl 属性不同。
解析器仍然形成一个由 OP_CONST 节点组成的 OP_LIST optree,但这些节点被传递给 class_apply_attributes 或 class_apply_field_attributes 函数。不是在正在解析的类中查找方法的类查找,而是使用已知属性的固定内部列表来查找将属性应用于类或字段的函数。将来,这可能会支持用户提供的扩展属性,但目前它只识别核心本身定义的属性。
在编译期间,解析器在解析字段的默认表达式时使用挂起的 compcv。类中所有字段的所有表达式共享同一个挂起的 compcv,然后将其编译到同一个内部 CV 中,该 CV 由构造函数调用以初始化该类提供的字段。
类本身的生成构造函数是一个 XSUB,它按顺序执行三个任务:它创建实例 SV 本身,调用字段初始化器,然后调用 ADJUST 块 CV。任何类的构造函数始终是相同的基本形状,无论该类是否有超类。
字段初始化器被收集到一个基于 optree 的生成 CV 中,称为字段初始化器 CV。这是包含字段初始化表达式的所有 optree 片段的 CV。调用时,字段初始化器 CV 可能会在调用所有单个字段初始化操作之前,对超类初始化器(如果存在)进行链式调用。字段初始化器 CV 使用堆栈上的两个项目调用;分别是实例 SV 和包含构造函数参数的直接 HV。请仔细注意:此 HV 是直接传递的,而不是通过 RV 引用传递的。之所以允许这样做,是因为调用者和被调用者都是直接生成的代码,而不是任意的纯 perl 子例程。
ADJUST 块 CV 全部收集到一个扁平列表中,还合并了超类定义的所有 CV。它们在字段初始化器 CV 之后按顺序全部调用。
$self访问当调用class_prepare_method_parse()时,它会安排新 CV 主体的填充区以一个名为$self的词法开始。由于填充区此时应是新创建的,因此它将具有填充区索引 1。该函数会检查这一点,如果为假,则会中止。
由于这一事实,方法或类似方法的 CV 主体中的代码可以可靠地使用填充区索引 1 来获取调用者引用。OP_INITFIELD操作码也依赖于这一事实。
类似地,在xhv_class_initfields_cv期间,下一个填充区插槽用于存储构造函数参数 HV,填充区索引为 2。
Paul Evans