內容

名稱

perlclassguts - feature 'class' 和類別語法的內部運作

說明

此文件提供有關 perl 解譯器如何實作 feature 'class' 語法和整體行為的深入資訊。它並非作為如何使用該功能的最終使用者指南。有關這方面的資訊,請參閱 perlclass

假設讀者大致熟悉 perl 解譯器的整體內部結構。有關這些詳細資訊的更一般概述,另請參閱 perlguts

資料儲存

類別

類別基本上是一個套件,並以與非類別套件完全相同的方式存在於符號表中,作為具有輔助結構的 HV。它與非類別套件的不同之處在於 HvSTASH_IS_CLASS() 巨集會對它傳回 true。

與其為類別相關的額外資訊儲存在附加到儲藏區的 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;

欄位

欄位在根本上仍然是在範圍中宣告的詞彙變數,而且存在於其對應 CV 的 PADNAMELIST 中。方法和其他類似方法的 CV 仍然可以完全擷取它們,就像它們可以對一般詞彙變數執行相同的動作一樣。欄位與其他類型的 pad 項目不同之處在於 PadnameIsFIELD() 巨集會對它傳回 true。

與其作為欄位相關的額外資訊會儲存在一個額外的結構中,可透過 padname 上的 PadnameFIELDINFO() 巨集存取。這個結構具有下列欄位

PADOFFSET  fieldix;
HV        *fieldstash;
OP        *defop;
SV        *paramname;
bool       def_if_undef;
bool       def_if_false;

方法

方法從本質上來說仍然是 CV,並且具有與 CV 相同的基本表示方式。它有一個 optree 和一個 pad,並透過其所包含套件的快取中的 GV 儲存。它與非方法 CV 的區別在於 CvIsMETHOD() 巨集將對其傳回 true。

(注意:此巨集不應與先前稱為 CvMETHOD() 的巨集混淆。該巨集與類別系統無關,並已重新命名為 CvNOWARN_AMBIGUOUS() 以避免此混淆。)

目前沒有需要儲存關於方法 CV 的額外資訊,因此結構不會新增任何新欄位。

實例

物件實例由一個全新的 SV 類型表示,其基本類型為 SVt_PVOBJ。這仍應祝福進入其類別快取,並以通常的方式包裝在 RV 中以供傳統物件使用。

由於這些是其自己的唯一容器類型,不同於雜湊或陣列,因此核心 builtin::reftype 函式在被詢問這些時會傳回一個新值。該值為 "OBJECT"

在內部,此類物件是一個 SV 指標陣列,其大小在建立時固定(因為類別中的欄位數在編譯後已知)。物件實例會儲存其內部的最大欄位索引(用於存取時的基礎錯誤檢查),以及儲存個別欄位值的固定大小 SV 指標陣列。

陣列和雜湊類型的欄位會直接將 AV 或 HV 指標儲存到陣列中;它們不會透過介入 RV 來儲存。

API

上述資料結構由下列 API 函式支援。

類別操作

class_setup_stash

void class_setup_stash(HV *stash);

當解析器遇到 class 關鍵字時呼叫。它將儲存升級為類別,並準備接收類別特定項目,例如方法和欄位。

class_seal_stash

void class_seal_stash(HV *stash);

class 區塊結束時或對於單元類別的包含範圍由解析器呼叫。此函數執行各種最後完成活動,在類別實例建構之前需要執行,但必須等到已知類別成員的所有資訊後才能執行。

必須在這兩個函數呼叫之間新增或修改正在編譯的類別。一旦類別已封裝,就無法修改。

class_add_field

void class_add_field(HV *stash, PADNAME *pn);

pad.c 呼叫,作為在目前儲存區中定義新欄位名稱的一部分。請注意,此函數不會建立儲存區名稱;這必須已由 pad.c 完成。此 API 函數僅通知類別已建立新欄位名稱,現在可供其使用。

class_add_ADJUST

void class_add_ADJUST(HV *stash, CV *cv);

當解析器已解析和建構新 ADJUST 區塊的 CV 後呼叫。這會新增到類別儲存的清單中。

欄位操作

class_prepare_initfield_parse

void class_prepare_initfield_parse();

在解析欄位變數的初始化運算式之前,由解析器呼叫。這會使用暫停的 compcv 將所有欄位初始化運算式組合到同一個 CV 中。

class_set_field_defop

void class_set_field_defop(PADNAME *pn, OPCODE defmode, OP *defop);

在解析器解析完欄位的初始化表達式後呼叫。設定預設表達式和應用模式。defmode 應為零,或根據預設模式為 OP_ORASSIGNOP_DORASSIGN 之一。

padadd_FIELD

#define padadd_FIELD

此旗標常數告訴 pad_add_name_* 函數系列,新的名稱應新增為欄位。不需要呼叫 class_add_field();這將自動完成。

方法操作

class_prepare_method_parse

void class_prepare_method_parse(CV *cv);

start_subparse() 之後,但在執行任何其他操作之前,由解析器呼叫。這會準備 PL_compcv 以解析方法;安排 CvIsMETHOD 測試為 true,新增 $self 詞彙,以及可能需要的任何其他活動。

class_wrap_method_body

OP *class_wrap_method_body(OP *o);

在將方法主體解析為 optree 的最後,但在將其包裝在最終 CV 中之前,由解析器呼叫。此函數會在 optree 中插入額外的 ops,以使方法正常運作。

物件實例

SVt_PVOBJ

#define SVt_PVOBJ

SvTYPE() 巨集比較時使用的 SV 類型常數。

ObjectMAXFIELD

SSize_t ObjectMAXFIELD(sv);

一個函式型巨集,取得可從 ObjectFIELDS 陣列存取的最大有效欄位索引。

ObjectFIELDS

SV **ObjectFIELDS(sv);

一個函式型巨集,直接從物件執行個體取得欄位陣列。欄位可透過其欄位索引存取,從 0 到 ObjectMAXFIELD 給定的最大有效索引。

OPCODES

OP_METHSTART

newUNOP_AUX(OP_METHSTART, ...);

一個 OP_METHSTARTUNOP_AUX,必須存在於方法 CV 的開頭,才能讓它正常運作。這是由 class_wrap_method_body() 插入的,甚至出現在與簽章引數檢查或萃取相關的任何 optree 片段之前。

此 op 負責將 $self 的值從引數清單中移出,並將方法需要存取的任何欄位變數繫結到 pad 中。AUX 向量將包含所需欄位/pad 索引配對的詳細資料。

此 op 也會對呼叫者值執行健全性檢查。它會檢查它是否絕對是相容類別型態的物件參考。如果不是,就會擲回例外狀況。

如果 op_private 欄位包含 OPpINITFIELDS 旗標,這表示 op 開始特殊 xhv_class_initfields_cv CV。在這種情況下,它應該另外從引數清單中取得第二個值,該值應該是純 HV 指標(直接,而非透過 RV),並將它繫結到第二個 pad 插槽,已產生 optree 會預期在那裡找到它。

OP_INITFIELD

OP_INITFIELD 僅在實例建構階段的 xhv_class_initfields_cv CV 中作為一部分被呼叫。這是組成實例的可變動欄位 (包含 AV 和 HV) 的個別 SV 實際上被指定到 ObjectFIELDS 陣列的時間。OPpINITFIELD_AVOPpINITFIELD_HV 私有旗標表示它是否正在建立 AV 或 HV;如果兩個都沒有設定,則建立 SV。

如果 op 具有 OPf_STACKED 旗標,它預期在堆疊中找到初始化值。對於 SV,這是資料堆疊中最上面的 SV。對於 AV 和 HV,它預期一個標記清單。

編譯時間行為

ADJUST Phaser

在編譯時間,ADJUST phaser 的剖析以與現有的 perl phaser (BEGIN 等) 根本不同的方式處理。

剖析器不是採取通常的路線,而是辨識 ADJUST 關鍵字引入了 phaser 區塊。然後,剖析器以類似於剖析 (匿名的) 方法主體的方式剖析此區塊的主體,建立一個沒有名稱 GV 的 CV。然後,透過呼叫 class_add_ADJUST 將其直接插入類別資訊中,完全繞過符號表。

屬性

在編譯期間,類別和欄位的屬性會以不同於現有 perl 屬性在子常式和詞彙變數上的方式處理。

剖析器仍會形成一個由 OP_CONST 節點組成的 OP_LIST optree,但這些節點會傳遞給 class_apply_attributesclass_apply_field_attributes 函式。剖析器並不會使用類別查詢來尋找正在剖析類別中的方法,而是使用已知屬性的內部固定清單來尋找函式,以將屬性套用至類別或欄位。未來可能會支援使用者提供的延伸屬性,但目前只會辨識核心本身定義的屬性。

欄位初始化表達式

在編譯期間,剖析器會在剖析欄位的預設表達式時使用暫停的 compcv。類別中所有欄位的表達式共用同一個暫停的 compcv,然後編譯成同一個內部 CV,由建構函式呼叫以初始化該類別提供的欄位。

執行時期行為

建構函式

類別本身產生的建構函數是一個 XSUB,它會依序執行三項工作:建立實例 SV 本身、呼叫欄位初始化程式,然後呼叫 ADJUST 區塊 CV。任何類別的建構函數永遠都是相同的基本形狀,不論類別是否有超類別。

欄位初始化程式會收集到一個稱為欄位初始化程式 CV 的已產生 optree 為基礎的 CV。這是包含欄位初始化表達式的所有 optree 片段的 CV。呼叫時,欄位初始化程式 CV 呼叫所有個別欄位初始化運算之前,可能會對超類別初始化程式(如果存在)進行連鎖呼叫。欄位初始化程式 CV 會在堆疊中呼叫兩個項目;實例 SV 和包含建構函數參數的直接 HV。請仔細注意:此 HV 是直接傳遞的,而不是透過 RV 參考。之所以允許這樣做,是因為呼叫者和被呼叫者都是直接產生的程式碼,而不是任意純 Perl 子常式。

ADJUST 區塊 CV 全部收集到一個單一的平面清單中,同時合併超類別定義的所有區塊。它們全部在欄位初始化程式 CV 之後依序呼叫。

方法期間的$self存取

呼叫class_prepare_method_parse()時,它會安排新 CV 主體的 pad 以稱為$self的詞彙開頭。由於 pad 應在此時新建立,因此它的 pad 索引將為 1。函數會檢查這一點,如果不為真則會中止。

由於這個事實,方法或類似方法 CV 主體內的程式碼可以可靠地使用 pad 索引 1 來取得呼叫者參考。OP_INITFIELD opcode 也依賴於這個事實。

類似地,在 xhv_class_initfields_cv 期間,下一個 pad 槽用於儲存建構函數參數 HV,在 pad 索引 2 中。

作者

Paul Evans