內容

名稱

perlguts - Perl API 簡介

說明

此文件嘗試說明如何使用 Perl API,並提供一些 Perl 核心基本運作方式的資訊。它遠未完成,而且可能包含許多錯誤。請將任何問題或意見反應給以下作者。

變數

資料類型

Perl 有三個型別定義處理 Perl 的三個主要資料類型

SV  Scalar Value
AV  Array Value
HV  Hash Value

每個型別定義都有特定常式來處理各種資料類型。

什麼是「IV」?

Perl 使用一個特殊的型別定義 IV,它是一個簡單的帶正負號整數型別,保證足夠大以容納指標(以及整數)。此外,還有 UV,它只是一個無正負號的 IV。

Perl 還使用幾個特殊的型別定義來宣告變數以容納(至少)給定大小的整數。使用 I8、I16、I32 和 I64 來宣告帶正負號的整數變數,其位元數至少與其名稱中的數字一樣多。這些都評估為最接近給定位元數的原生 C 型別,但不會小於該數字。例如,在許多平台上,short 的長度為 16 位元,如果是這樣,I16 將評估為 short。但在 short 不完全是 16 位元的平台上,Perl 將使用包含 16 位元或更多位元的最小型別。

U8、U16、U32 和 U64 用於宣告對應的無正負號整數型別。

如果平台不支援 64 位元整數,則 I64 和 U64 都將是未定義的。使用 IV 和 UV 來宣告最大的可行整數,並使用 "WIDEST_UTYPE" in perlapi 來宣告絕對最大的無正負號整數,但可能無法在所有情況下使用。

可以使用 "INT16_C" in perlapi"UINTMAX_C" in perlapi 和類似的函式來指定數字常數。

使用 SV

可以使用一個指令建立並載入 SV。有五種類型的值可以載入:整數值 (IV)、無符號整數值 (UV)、雙精度浮點數 (NV)、字串 (PV) 和另一個純量 (SV)。(「PV」代表「指標值」。你可能會認為這個名稱不恰當,因為它被描述為僅指向字串。然而,它有可能指向其他東西。例如,它可以指向 UV 陣列。但是,將它用於非字串需要小心,因為內部許多假設的基礎是 PV 僅用於字串。例如,通常會自動附加一個尾隨的 NUL。非字串用法僅記載於本段落中。)

七個常式為

SV*  newSViv(IV);
SV*  newSVuv(UV);
SV*  newSVnv(double);
SV*  newSVpv(const char*, STRLEN);
SV*  newSVpvn(const char*, STRLEN);
SV*  newSVpvf(const char*, ...);
SV*  newSVsv(SV*);

STRLEN 是一種整數類型(Size_t,通常在 config.h 中定義為 size_t),保證足夠大以表示 Perl 可以處理的任何字串大小。

在 SV 需要更複雜初始化的不常見情況下,你可以使用 newSV(len) 建立一個空的 SV。如果 len 為 0,則會傳回類型為 NULL 的空 SV,否則會傳回類型為 PV 的 SV,並配置 len + 1(用於 NUL)位元組的儲存空間,可透過 SvPVX 存取。在兩種情況下,SV 都具有未定義的值。

SV *sv = newSV(0);   /* no storage allocated  */
SV *sv = newSV(10);  /* 10 (+1) bytes of uninitialised storage
                      * allocated */

若要變更已存在 SV 的值,有八個常式

void  sv_setiv(SV*, IV);
void  sv_setuv(SV*, UV);
void  sv_setnv(SV*, double);
void  sv_setpv(SV*, const char*);
void  sv_setpvn(SV*, const char*, STRLEN)
void  sv_setpvf(SV*, const char*, ...);
void  sv_vsetpvfn(SV*, const char*, STRLEN, va_list *,
                                    SV **, Size_t, bool *);
void  sv_setsv(SV*, SV*);

請注意,你可以選擇使用 sv_setpvnnewSVpvnnewSVpv 指定要指派的字串長度,或者你可以讓 Perl 使用 sv_setpv 計算長度,或將 0 指定為 newSVpv 的第二個引數。不過,請注意,Perl 將使用 strlen 判斷字串長度,這取決於字串是否以 NUL 字元結尾,且不包含其他 NUL。

sv_setpvf 的引數會像 sprintf 一樣處理,而格式化的輸出會變成值。

sv_vsetpvfn 類似於 vsprintf,但它允許您指定指向變數引數清單的指標或 SV 陣列的位址和長度。最後一個引數指向一個布林值;在回傳時,如果該布林值為真,則已使用特定於地區的資訊來格式化字串,因此字串的內容不可靠(請參閱 perlsec)。如果該資訊不重要,此指標可以為 NULL。請注意,此函式需要您指定格式的長度。

sv_set*() 函式不夠通用,無法對具有「魔法」的值進行操作。請參閱本文稍後部分的 "魔法虛擬表格"

所有包含字串的 SV 都應以 NUL 字元結尾。如果它不是以 NUL 結尾,則會導致核心傾印和損毀,因為程式碼會將字串傳遞給 C 函式或系統呼叫,而這些函式或呼叫需要以 NUL 結尾的字串。Perl 自身的函式通常會因此加入尾隨 NUL。儘管如此,當您將儲存在 SV 中的字串傳遞給 C 函式或系統呼叫時,您仍應非常小心。

為了存取 SV 指向的實際值,Perl 的 API 公開了幾個巨集,這些巨集將實際的純量類型強制轉換為 IV、UV、double 或字串

如果您想要知道標量值是否為 TRUE,您可以使用

SvTRUE(SV*)

雖然 Perl 會自動為您擴充字串,但如果您需要強制 Perl 為您的 SV 分配更多記憶體,您可以使用巨集

SvGROW(SV*, STRLEN newlen)

它會判斷是否需要分配更多記憶體。如果是,它會呼叫函式 sv_grow。請注意,SvGROW 只能增加,不能減少 SV 的已分配記憶體,而且它不會自動為尾隨 NUL 位元組新增空間(perl 自有的字串函式通常執行 SvGROW(sv, len + 1))。

如果您想要寫入現有 SV 的緩衝區並將其值設定為字串,請使用 SvPVbyte_force() 或其變體之一,以強制 SV 成為 PV。這會從 SV 中移除任何不同類型的非字串性,同時保留 PV 中 SV 的內容。例如,這可用於將 API 函式的資料附加到緩衝區,而無需額外複製

(void)SvPVbyte_force(sv, len);
s = SvGROW(sv, len + needlen + 1);
/* something that modifies up to needlen bytes at s+len, but
   modifies newlen bytes
     eg. newlen = read(fd, s + len, needlen);
   ignoring errors for these examples
 */
s[len + newlen] = '\0';
SvCUR_set(sv, len + newlen);
SvUTF8_off(sv);
SvSETMAGIC(sv);

如果您已經在記憶體中有資料,或者您想要讓您的程式碼保持簡單,您可以使用 sv_cat*() 變體之一,例如 sv_catpvn()。如果您想要插入字串中的任何位置,您可以使用 sv_insert() 或 sv_insert_flags()。

如果您不需要 SV 的現有內容,您可以使用

避免一些複製

SvPVCLEAR(sv);
s = SvGROW(sv, needlen + 1);
/* something that modifies up to needlen bytes at s, but modifies
   newlen bytes
     eg. newlen = read(fd, s, needlen);
 */
s[newlen] = '\0';
SvCUR_set(sv, newlen);
SvPOK_only(sv); /* also clears SVf_UTF8 */
SvSETMAGIC(sv);

同樣地,如果您已經在記憶體中擁有資料或想要避免上述的複雜性,您可以使用 sv_setpvn()。

如果您有一個使用 Newx() 分配的緩衝區,並且想要將它設定為 SV 的值,您可以使用 sv_usepvn_flags()。如果您想要避免 perl 重新分配緩衝區以符合尾隨 NUL,則有一些要求

Newx(buf, somesize+1, char);
/* ... fill in buf ... */
buf[somesize] = '\0';
sv_usepvn_flags(sv, buf, somesize, SV_SMAGIC | SV_HAS_TRAILING_NUL);
/* buf now belongs to perl, don't release it */

如果您有一個 SV,並且想要知道 Perl 認為其中儲存了哪種類型的資料,您可以使用以下巨集來檢查您擁有的 SV 類型。

SvIOK(SV*)
SvNOK(SV*)
SvPOK(SV*)

請注意,檢索 SV 的數值可能會在該 SV 上設定 IOK 或 NOK,即使 SV 最初為字串。在 Perl 5.36.0 之前,檢索整數的字串值可能會設定 POK,但這不再會發生。從 5.36.0 開始,這可以用來區分 SV 的原始表示,並且旨在讓序列化器的生活更簡單

/* references handled elsewhere */
if (SvIsBOOL(sv)) {
    /* originally boolean */
    ...
}
else if (SvPOK(sv)) {
    /* originally a string */
    ...
}
else if (SvNIOK(sv)) {
    /* originally numeric */
    ...
}
else {
    /* something special or undef */
}

您可以使用以下巨集取得並設定儲存在 SV 中的字串的目前長度

SvCUR(SV*)
SvCUR_set(SV*, I32 val)

您也可以使用巨集取得指向儲存在 SV 中的字串結尾的指標

SvEND(SV*)

但請注意,這最後三個巨集僅在 SvPOK() 為 true 時才有效。

如果您想要附加一些內容到儲存在 SV* 中的字串結尾,您可以使用以下函式

void  sv_catpv(SV*, const char*);
void  sv_catpvn(SV*, const char*, STRLEN);
void  sv_catpvf(SV*, const char*, ...);
void  sv_vcatpvfn(SV*, const char*, STRLEN, va_list *, SV **,
                                                         I32, bool);
void  sv_catsv(SV*, SV*);

第一個函式使用 strlen 計算要附加的字串長度。在第二個函式中,您自己指定字串長度。第三個函式處理其引數就像 sprintf,並附加格式化的輸出。第四個函式的工作方式類似於 vsprintf。您可以指定 SV 陣列的地址和長度,而不是 va_list 引數。第五個函式使用儲存在第二個 SV 中的字串來延伸儲存在第一個 SV 中的字串。它也會強制第二個 SV 被解釋為字串。

sv_cat*() 函式不夠通用,無法對具有「魔法」的值進行操作。請參閱本文檔後面的 「魔法虛擬表格」

如果您知道一個純量變數的名稱,您可以使用以下方式取得指向其 SV 的指標

SV*  get_sv("package::varname", 0);

如果變數不存在,這會傳回 NULL。

如果您想要知道這個變數(或任何其他 SV)是否實際上是 defined,您可以呼叫

SvOK(SV*)

標量 undef 值儲存在稱為 PL_sv_undef 的 SV 實例中。

當需要 SV* 時,可以使用其位址。請務必不要嘗試將隨機 sv 與 &PL_sv_undef 進行比較。例如,在介接 Perl 程式碼時,它將正確執行

foo(undef);

但當呼叫為時將無法執行

$x = undef;
foo($x);

因此,請重複使用 SvOK() 檢查 sv 是否已定義。

此外,在將 &PL_sv_undef 用作 AV 或 HV 中的值時,您必須小心(請參閱 "AV、HV 和未定義值")。

還有兩個值 PL_sv_yesPL_sv_no,它們分別包含布林值 TRUE 和 FALSE。與 PL_sv_undef 一樣,當需要 SV* 時,可以使用它們的位址。

不要誤以為 (SV *) 0&PL_sv_undef 相同。請看這段程式碼

SV* sv = (SV*) 0;
if (I-am-to-return-a-real-value) {
        sv = sv_2mortal(newSViv(42));
}
sv_setsv(ST(0), sv);

這段程式碼嘗試傳回一個新的 SV(包含值 42),如果它應該傳回一個真實值,否則傳回未定義。相反,它傳回了一個 NULL 指標,這將在某個地方導致分段錯誤、總線錯誤或只是奇怪的結果。將第一行的零變更為 &PL_sv_undef,一切都會好轉。

若要釋放您建立的 SV,請呼叫 SvREFCNT_dec(SV*)。通常不需要此呼叫(請參閱 "參考計數和死亡率")。

偏移量

Perl 提供函式 sv_chop,可有效地從字串開頭移除字元;您給它一個 SV 和一個指向 PV 內部某個位置的指標,它會捨棄指標之前的所有內容。效率來自於一個小技巧:sv_chop 實際上並未移除字元,而是設定旗標 OOK(偏移量確定)以向其他函式發出偏移量技巧生效的訊號,並將 PV 指標(稱為 SvPVX)向前移動被切斷的位元組數,並相應調整 SvCURSvLEN。(舊 PV 指標和新 PV 指標之間的部分空間用於儲存切斷位元組的計數。)

因此,在這個時候,我們配置的緩衝區開頭位於記憶體中的 SvPVX(sv) - SvIV(sv),而 PV 指標指向這個配置儲存空間的中央。

最好透過範例來說明。通常,寫入時複製會阻止替換算子使用這個技巧,但如果您能建立一個無法寫入時複製的字串,您就可以看到它發揮作用。在目前的實作中,字串緩衝區的最後一個位元組用作寫入時複製參考計數。如果緩衝區不夠大,則會略過寫入時複製。首先看一個空字串

% ./perl -Ilib -MDevel::Peek -le '$a=""; $a .= ""; Dump $a'
SV = PV(0x7ffb7c008a70) at 0x7ffb7c030390
  REFCNT = 1
  FLAGS = (POK,pPOK)
  PV = 0x7ffb7bc05b50 ""\0
  CUR = 0
  LEN = 10

在此處注意 LEN 為 10。(在您的平台上可能有所不同。)將字串長度延伸為小於 10 的長度,並進行替換

% ./perl -Ilib -MDevel::Peek -le '$a=""; $a.="123456789"; $a=~s/.//; \
                                                           Dump($a)'
SV = PV(0x7ffa04008a70) at 0x7ffa04030390
  REFCNT = 1
  FLAGS = (POK,OOK,pPOK)
  OFFSET = 1
  PV = 0x7ffa03c05b61 ( "\1" . ) "23456789"\0
  CUR = 8
  LEN = 9

在此處,切除的位元組數目 (1) 接著顯示為 OFFSET。字串中「真實」與「虛假」開頭之間的部分顯示在括號中,而 SvCURSvLEN 的值反映虛假開頭,而非真實開頭。(字串緩衝區的第一個字元在此處已變更為「\1」,而非「1」,因為目前的實作會將偏移量計數儲存在字串緩衝區中。這可能會變更。)

在 AV 上執行類似於偏移量駭入的動作,以啟用有效率的位移和從陣列開頭進行拼接;而 AvARRAY 指向 Perl 可見的陣列中第一個元素,AvALLOC 指向 C 陣列的真實開頭。這些通常相同,但可以透過將 AvARRAY 增加 1 並減少 AvFILLAvMAX 來執行 shift 作業。同樣地,C 陣列真實開頭的位置只會在釋放陣列時發揮作用。請參閱 av.c 中的 av_shift

SV 中實際儲存了什麼?

請回想,判斷您擁有的純量類型的慣用方法是使用 Sv*OK 巨集。由於純量可以同時是數字和字串,因此通常這些巨集將永遠傳回 TRUE,而呼叫 Sv*V 巨集將執行適當的字串轉換為整數/雙精度或整數/雙精度轉換為字串。

如果您真的需要知道在 SV 中是否有整數、雙精度或字串指標,您可以改用以下三個巨集

SvIOKp(SV*)
SvNOKp(SV*)
SvPOKp(SV*)

這些巨集會告訴您在 SV 中是否真正儲存了整數、雙精度或字串指標。「p」代表私人。

私人和公開標記可能有所不同的方式有很多。例如,在 perl 5.16 及更早版本中,繫結的 SV 可能在 IV 槽中具有有效的基礎值(因此 SvIOKp 為 true),但應透過 FETCH 常式而非直接存取資料,因此 SvIOK 為 false。(在 perl 5.18 及後續版本中,繫結的純量使用標記的方式與未繫結的純量相同。)另一個情況是發生數字轉換且精確度遺失時:只有私人標記會設定在「有損失」值上。因此,當 NV 轉換為 IV 時會造成損失,SvIOKp、SvNOKp 和 SvNOK 將會設定,而 SvIOK 則不會。

不過,一般來說,最好使用 Sv*V 巨集。

使用 AV

建立和載入 AV 有兩種主要且長久的方法。第一種方法建立一個空的 AV

AV*  newAV();

第二種方法同時建立 AV 和最初用 SV 填充它

AV*  av_make(SSize_t num, SV **ptr);

第二個引數指向包含 numSV* 的陣列。一旦建立 AV,就可以視需要刪除 SV。

Perl v5.36 新增了兩種建立 AV 和配置 SV** 陣列而不填充它的方法。這些方法比 newAV() 接著 av_extend() 更有效率。

/* Creates but does not initialize (Zero) the SV** array */
AV *av = newAV_alloc_x(1);
/* Creates and does initialize (Zero) the SV** array */
AV *av = newAV_alloc_xz(1);

數字引數是指要配置的陣列元素數,而不是陣列索引,而且必須大於 0。第一種形式只能在所有元素在任何讀取發生之前都已初始化時使用。讀取未初始化的 SV*,也就是將隨機記憶體位址當成 SV*,是一個嚴重的錯誤。

一旦建立 AV,就可以對它執行下列操作

void  av_push(AV*, SV*);
SV*   av_pop(AV*);
SV*   av_shift(AV*);
void  av_unshift(AV*, SSize_t num);

這些應該是熟悉的操作,除了 av_unshift。這個常式在陣列前面新增 num 個元素,其值為 undef。然後必須使用 av_store(如下所述)將值指定給這些新元素。

以下是一些其他函式

SSize_t av_top_index(AV*);
SV**    av_fetch(AV*, SSize_t key, I32 lval);
SV**    av_store(AV*, SSize_t key, SV* val);

av_top_index 函式傳回陣列中最高的索引值(就像 Perl 中的 $#array)。如果陣列是空的,則傳回 -1。av_fetch 函式傳回索引 key 的值,但如果 lval 非零,則 av_fetch 會在該索引儲存一個 undef 值。av_store 函式將值 val 儲存在索引 key,而且不會增加 val 的參考計數。因此呼叫者有責任處理這件事,而且如果 av_store 傳回 NULL,則呼叫者必須減少參考計數以避免記憶體外洩。請注意 av_fetchav_store 都傳回 SV**,而不是 SV* 作為其傳回值。

再補充一些

void  av_clear(AV*);
void  av_undef(AV*);
void  av_extend(AV*, SSize_t key);

av_clear 函式刪除 AV* 陣列中的所有元素,但不會實際刪除陣列本身。av_undef 函式會刪除陣列中的所有元素以及陣列本身。av_extend 函式延伸陣列,使其至少包含 key+1 個元素。如果 key+1 小於陣列目前配置的長度,則不會執行任何動作。

如果您知道陣列變數的名稱,您可以使用下列方式取得其 AV 指標

AV*  get_av("package::varname", 0);

如果變數不存在,這會傳回 NULL。

請參閱 "了解繫結雜湊和陣列的魔力" 以取得有關如何對繫結陣列使用陣列存取函數的更多資訊。

更有效率地處理新的或純 AV

Perl v5.36 和 v5.38 引入了部分函數的精簡內嵌版本

這些是直接替換,但只能用於符合下列條件的直觀 AV

使用 newAV()av_makenewAV_alloc_xnewAV_alloc_xz 建立的 AV 在建立時都相容。只有在宣告為唯讀或非真實、附加了魔法或以其他方式進行異常設定時,它們才會停止相容。

請注意,部分直譯器函數可能會在正常運作時將魔法附加到 AV。因此,除非您確定 AV 的生命週期,否則最安全的方法是在接近 AV 建立點時才使用這些新函數。

處理 HV

若要建立 HV,請使用下列常式

HV*  newHV();

建立 HV 後,可以在其上執行下列操作

SV**  hv_store(HV*, const char* key, U32 klen, SV* val, U32 hash);
SV**  hv_fetch(HV*, const char* key, U32 klen, I32 lval);

klen 參數是傳入金鑰的長度 (請注意,您無法將 0 傳入作為 klen 的值,以告知 Perl 測量金鑰的長度)。val 參數包含要儲存的純量的 SV 指標,而 hash 是預先計算的雜湊值 (如果您希望 hv_store 為您計算,則為零)。lval 參數指出此擷取實際上是否為儲存操作的一部分,在這種情況下,將會使用提供的金鑰將新的未定義值新增到 HV,而 hv_fetch 將會回傳,就像該值已經存在一樣。

請記住,hv_storehv_fetch 會回傳 SV**,而不仅仅是 SV*。若要存取純量值,您必須先取消傳回值的參照。但是,您應該在取消參照之前檢查傳回值是否為 NULL。

這兩個函數的第一個會檢查雜湊表條目是否存在,而第二個會將其刪除。

bool  hv_exists(HV*, const char* key, U32 klen);
SV*   hv_delete(HV*, const char* key, U32 klen, I32 flags);

如果 flags 不包含 G_DISCARD 旗標,則 hv_delete 會建立已刪除值的暫存副本並傳回。

以及更多雜項函數

void   hv_clear(HV*);
void   hv_undef(HV*);

如同其 AV 對應函數,hv_clear 會刪除雜湊表中的所有條目,但不會實際刪除雜湊表。hv_undef 會刪除條目和雜湊表本身。

Perl 會將實際資料保留在結構的連結清單中,其類型定義為 HE。這些結構包含實際的鍵和值指標(以及額外的管理開銷)。鍵是字串指標;值是 SV*。不過,一旦您擁有 HE*,請使用以下指定的常式取得實際的鍵和值。

    I32    hv_iterinit(HV*);
            /* Prepares starting point to traverse hash table */
    HE*    hv_iternext(HV*);
            /* Get the next entry, and return a pointer to a
               structure that has both the key and value */
    char*  hv_iterkey(HE* entry, I32* retlen);
            /* Get the key from an HE structure and also return
               the length of the key string */
    SV*    hv_iterval(HV*, HE* entry);
            /* Return an SV pointer to the value of the HE
               structure */
    SV*    hv_iternextsv(HV*, char** key, I32* retlen);
            /* This convenience routine combines hv_iternext,
	       hv_iterkey, and hv_iterval.  The key and retlen
	       arguments are return values for the key and its
	       length.  The value is returned in the SV* argument */

如果您知道雜湊變數的名稱,您可以使用下列方式取得其 HV 指標

HV*  get_hv("package::varname", 0);

如果變數不存在,這會傳回 NULL。

雜湊演算法定義在 PERL_HASH 巨集中

PERL_HASH(hash, key, klen)

此巨集的確切實作會因 Perl 的架構和版本而異,且傳回值可能會因呼叫而異,因此該值僅在單一 Perl 程序的期間內有效。

請參閱 "了解繫結雜湊和陣列的魔力",以取得有關如何對繫結雜湊使用雜湊存取函數的更多資訊。

雜湊 API 延伸

從版本 5.004 開始,也支援下列函數

HE*     hv_fetch_ent  (HV* tb, SV* key, I32 lval, U32 hash);
HE*     hv_store_ent  (HV* tb, SV* key, SV* val, U32 hash);

bool    hv_exists_ent (HV* tb, SV* key, U32 hash);
SV*     hv_delete_ent (HV* tb, SV* key, I32 flags, U32 hash);

SV*     hv_iterkeysv  (HE* entry);

請注意,這些函數會採用 SV* 鍵,這簡化了撰寫處理雜湊結構的延伸程式碼。這些函數也允許將 SV* 鍵傳遞給 tie 函數,而無需強制您將鍵字串化(與前一組函數不同)。

它們也會傳回並接受完整的雜湊項目 (HE*),讓它們的使用更有效率 (因為特定字串的雜湊號碼不必每次都重新計算)。請參閱 perlapi 以取得詳細說明。

以下巨集必須永遠用於存取雜湊項目的內容。請注意,這些巨集的引數必須是簡單變數,因為它們可能會被評估多次。請參閱 perlapi 以取得這些巨集的詳細說明。

HePV(HE* he, STRLEN len)
HeVAL(HE* he)
HeHASH(HE* he)
HeSVKEY(HE* he)
HeSVKEY_force(HE* he)
HeSVKEY_set(HE* he, SV* sv)

定義了這兩個較低層級的巨集,但僅限於處理非 SV* 的金鑰時使用

HeKEY(HE* he)
HeKLEN(HE* he)

請注意,hv_storehv_store_ent 都不會增加已儲存 val 的參考計數,這是呼叫者的責任。如果這些函式傳回 NULL 值,呼叫者通常必須減少 val 的參考計數,以避免記憶體外洩。

AV、HV 和未定義值

有時你必須在 AV 或 HV 中儲存未定義值。雖然這可能是罕見的情況,但可能會很棘手。這是因為如果你需要未定義的 SV,你習慣使用 &PL_sv_undef

例如,直覺會告訴你這個 XS 程式碼

AV *av = newAV();
av_store( av, 0, &PL_sv_undef );

等於這個 Perl 程式碼

my @av;
$av[0] = undef;

很遺憾,這並不正確。在 perl 5.18 及更早版本中,AV 使用 &PL_sv_undef 作為標記,表示陣列元素尚未初始化。因此,對於上述 Perl 程式碼,exists $av[0] 會為真,但對於 XS 程式碼產生的陣列則為假。在 perl 5.20 中,儲存 &PL_sv_undef 會建立唯讀元素,因為儲存的是純量 &PL_sv_undef 本身,而不是副本。

在 HV 中儲存 &PL_sv_undef 時可能會發生類似問題

hv_store( hv, "key", 3, &PL_sv_undef, 0 );

這確實會讓值變成 undef,但如果你嘗試修改 key 的值,你會收到以下錯誤

Modification of non-creatable hash value attempted

在 perl 5.8.0 中,&PL_sv_undef 也用於標記受限雜湊中的佔位符。這導致雜湊項目在反覆處理雜湊或使用 hv_exists 函式檢查金鑰時不會出現。

當您將 &PL_sv_yes&PL_sv_no 儲存在 AV 或 HV 中時,您可能會遇到類似的問題。嘗試修改此類元素會產生以下錯誤

Modification of a read-only value attempted

簡而言之,您可以對 AV 和 HV 使用特殊變數 &PL_sv_undef&PL_sv_yes&PL_sv_no,但您必須確定您知道自己在做什麼。

一般來說,如果您想要在 AV 或 HV 中儲存未定義的值,您不應該使用 &PL_sv_undef,而應該使用 newSV 函數建立新的未定義值,例如

av_store( av, 42, newSV(0) );
hv_store( hv, "foo", 3, newSV(0), 0 );

參照

參照是一種特殊類型的標量,指向其他資料類型(包括其他參照)。

若要建立參照,請使用下列函數之一

SV* newRV_inc((SV*) thing);
SV* newRV_noinc((SV*) thing);

thing 參數可以是任何 SV*AV*HV*。這些函數是相同的,但 newRV_inc 會增加 thing 的參照計數,而 newRV_noinc 則不會。由於歷史原因,newRVnewRV_inc 的同義詞。

一旦您有了一個參照,您可以使用下列巨集來取消參照

SvRV(SV*)

然後呼叫適當的常式,將傳回的 SV* 轉換為 AV*HV*(如果需要)。

若要判斷 SV 是否為參照,您可以使用下列巨集

SvROK(SV*)

若要找出參照所指值的類型,請使用下列巨集,然後檢查傳回值。

SvTYPE(SvRV(SV*))

傳回的最有用的類型為

SVt_PVAV    Array
SVt_PVHV    Hash
SVt_PVCV    Code
SVt_PVGV    Glob (possibly a file handle)

傳回的任何數值小於 SVt_PVAV 都會是某種形式的標量。

請參閱 perlapi 中的「svtype」 以取得更多詳細資料。

Blessed 參照和類別物件

參照也用於支援物件導向程式設計。在 perl 的 OO 詞彙中,物件只是一個已 blessed 到套件(或類別)中的參照。一旦 blessed,程式設計人員現在可以使用參照來存取類別中的各種方法。

可以使用下列函數將參照 blessed 到套件中

SV* sv_bless(SV* sv, HV* stash);

sv 參數必須是參照值。stash 參數指定參照將屬於哪個類別。請參閱 「Stashes and Globs」 以取得有關將類別名稱轉換為 stashes 的資訊。

/* 仍處於建構中 */

如果 rv 尚未為參照,下列函數會將其升級為參照。建立新的 SV 供 rv 指向。如果 classname 為非 null,SV 會 blessed 到指定的類別中。傳回 SV。

SV* newSVrv(SV* rv, const char* classname);

下列三個函數會將整數、無符號整數或雙精度數字複製到 SV 中,其參照為 rv。如果 classname 為非 null,會 blessed SV。

SV* sv_setref_iv(SV* rv, const char* classname, IV iv);
SV* sv_setref_uv(SV* rv, const char* classname, UV uv);
SV* sv_setref_nv(SV* rv, const char* classname, NV iv);

下列函數會將指標值(位址,而非字串!)複製到 SV 中,其參照為 rv。如果 classname 為非 null,會 blessed SV。

SV* sv_setref_pv(SV* rv, const char* classname, void* pv);

下列函數會將字串複製到 SV 中,其參照為 rv。將長度設定為 0 以讓 Perl 計算字串長度。如果 classname 為非 null,會 blessed SV。

SV* sv_setref_pvn(SV* rv, const char* classname, char* pv,
                                                     STRLEN length);

下列函數會測試 SV 是否 blessed 到指定的類別中。它不會檢查繼承關係。

int  sv_isa(SV* sv, const char* name);

下列函式測試 SV 是否為受祝福物件的參考。

int  sv_isobject(SV* sv);

下列函式測試 SV 是否衍生自指定的類別。SV 可以是受祝福物件的參考,也可以是包含類別名稱的字串。這是實作 UNIVERSAL::isa 功能的函式。

bool sv_derived_from(SV* sv, const char* name);

若要檢查您是否取得衍生自特定類別的物件,您必須撰寫

if (sv_isobject(sv) && sv_derived_from(sv, class)) { ... }

建立新變數

若要建立新的 Perl 變數,其值為 undef,且可從您的 Perl 腳本存取,請使用下列常式,具體取決於變數類型。

SV*  get_sv("package::varname", GV_ADD);
AV*  get_av("package::varname", GV_ADD);
HV*  get_hv("package::varname", GV_ADD);

請注意將 GV_ADD 用作第二個參數。現在可以使用適於資料類型的常式設定新變數。

有其他巨集,其值可以與 GV_ADD 參數進行按位元 OR,以啟用某些額外功能。這些位元是

GV_ADDMULTI

將變數標記為多重定義,從而防止

Name <varname> used only once: possible typo

警告。

GV_ADDWARN

發出警告

Had to create <varname> unexpectedly

如果在呼叫函式之前變數不存在。

如果您未指定套件名稱,則會在目前套件中建立變數。

參考計數和死亡率

Perl 使用參考計數驅動的垃圾回收機制。SV、AV 或 HV(以下簡稱 xV)的生命週期開始時,參考計數為 1。如果 xV 的參考計數降至 0,則會將其銷毀,並釋出其記憶體以供重複使用。在最基本的內部層級,可以使用下列巨集操作參考計數

int SvREFCNT(SV* sv);
SV* SvREFCNT_inc(SV* sv);
void SvREFCNT_dec(SV* sv);

(對於這些基本巨集的完整通用性可以換取一些效能的情況,也有增量和減量巨集的後綴版本。)

然而,程式設計師思考參考的方式,並非那麼多在於裸露的參考計數,而是參考的擁有權。對 xV 的參考可以由各種實體擁有:另一個 xV、Perl 解譯器、XS 資料結構、一段正在執行的程式碼,或動態範圍。xV 通常不知道哪些實體擁有對它的參考;它只知道有多少個參考,也就是參考計數。

為了正確維護參考計數,追蹤 XS 程式碼正在處理哪些參考至關重要。程式設計師應該始終知道參考從何而來,以及誰擁有它,並注意任何參考的建立或銷毀,以及任何擁有權的轉移。由於擁有權並未明確表示在 xV 資料結構中,因此只有參考計數需要實際由程式碼維護,這表示對擁有權的理解實際上並未在程式碼中顯現。例如,將參考的擁有權從一個擁有者轉移到另一個擁有者並不會改變參考計數,因此可以不使用任何實際程式碼來達成。(轉移程式碼不會觸及被參考的物件,但需要確保前一個擁有者知道它不再擁有該參考,而新的擁有者知道它現在擁有該參考。)

在 Perl 層級中可見的 xV 不應取消參照,因此不會被銷毀。通常,物件僅在不可見時才會取消參照,通常透過使其不可見的方法來取消參照。例如,Perl 參照值 (RV) 擁有其所指物件的參照,因此如果 RV 被覆寫,該參照就會被銷毀,而不可再存取的所指物件也可能因此被銷毀。

許多函數在目的中包含某種參照處理。有時這會以參照擁有權的觀點來記錄,而有時則會以參照計數變更的觀點來記錄(較不具幫助)。例如,newRV_inc() 函數記錄為建立新的 RV(參照計數為 1),並增加呼叫者所提供的所指物件的參照計數。這最能理解為建立新的所指物件參照,該參照由建立的 RV 擁有,並將 RV 的唯一參照擁有權傳回呼叫者。newRV_noinc() 函數則不會增加所指物件的參照計數,但 RV 仍會擁有對所指物件的參照。因此,隱含地表示 newRV_noinc() 的呼叫者會放棄對所指物件的參照,即使它對資料結構的影響較小,這在概念上也是較為複雜的運算。

例如,想像一下您想要從 XSUB 函數傳回一個參照。在 XSUB 常式中,您建立一個 SV,它最初只有一個參照,由 XSUB 常式擁有。在常式完成之前,需要處置這個參照,否則它會洩漏,防止 SV 被銷毀。因此,要建立一個參照 SV 的 RV,最方便的方法是將 SV 傳遞給 newRV_noinc(),它會使用那個參照。現在,XSUB 常式不再擁有對 SV 的參照,但擁有對 RV 的參照,而 RV 反過來擁有對 SV 的參照。然後,透過從 XSUB 傳回 RV 的程序,轉移對 RV 的參照的所有權。

有一些方便的函數可用來協助銷毀 xV。這些函數引入了「死亡率」的概念。許多文件說明 xV 本身是會死的,但這具有誤導性。實際上是 xV 的參照會死,而且有可能對單一 xV 有多個會死的參照。對參照而言,會死表示它是由 temps 堆疊擁有,這是 perl 的許多內部堆疊之一,它會在「稍後」銷毀該參照。通常,「稍後」是指當前 Perl 陳述式的結尾。然而,在動態範圍周圍會變得更複雜:可能有多組會死的參照同時存在,死亡日期不同。在內部,決定會死的 xV 參照何時銷毀的實際決定因素取決於兩個巨集,SAVETMPS 和 FREETMPS。請參閱 perlcallperlxs 以及下方的「暫存堆疊」,以取得關於這些巨集的更多詳細資料。

會死的參照主要用於放置在 perl 主堆疊上的 xV。堆疊對於參照追蹤而言是有問題的,因為它包含許多 xV 參照,但並不擁有那些參照:它們沒有被計算在內。目前,由於堆疊未計算的參照不足以讓 xV 保持運作,因此有許多錯誤是由於 xV 在堆疊參照時被銷毀所造成的。因此,在堆疊上放置(未計算的)參照時,非常重要的是要確保對同一個 xV 有一個計算的參照,而且至少會持續與未計算的參照一樣長的時間。但讓那個計算的參照在適當的時間清除,並且不要過度延長 xV 的生命也很重要。要有一個會死的參照通常是滿足此需求的最佳方式,特別是如果 xV 是特別建立來放置在堆疊上,否則將不會被參照。

要建立一個會死的參照,請使用函數

SV*  sv_newmortal()
SV*  sv_mortalcopy(SV*)
SV*  sv_2mortal(SV*)

sv_newmortal() 建立一個 SV(具有未定義值),其唯一參考是 mortal。sv_mortalcopy() 建立一個 xV,其值是所提供 xV 的副本,且其唯一參考是 mortal。sv_2mortal() 將現有的 xV 參考設為 mortal:它將參考的所有權從呼叫者轉移到 temps 堆疊。由於 sv_newmortal 沒有給予新的 SV 任何值,因此通常必須透過 sv_setpvsv_setiv 等給予它一個值。

SV *tmp = sv_newmortal();
sv_setiv(tmp, an_integer);

由於那是多個 C 陳述式,因此很常見看到使用此慣用語法

SV *tmp = sv_2mortal(newSViv(an_integer));

mortal 常式不僅適用於 SV;AV 和 HV 可以透過將其位址(類型轉換為 SV*)傳遞給 sv_2mortalsv_mortalcopy 常式,以設為 mortal。

Stashes 和 Globs

stash 是包含在套件中定義的所有變數的雜湊。stash 的每個鍵都是符號名稱(由具有相同名稱的所有不同類型的物件共用),雜湊表中的每個值都是 GV(Glob 值)。此 GV 反過來包含對該名稱的各種物件的參考,包括(但不限於)下列

Scalar Value
Array Value
Hash Value
I/O Handle
Format
Subroutine

有一個稱為 PL_defstash 的單一 stash,其中包含存在於 main 套件中的項目。若要取得其他套件中的項目,請將字串「::」附加到套件名稱。Foo 套件中的項目位於 PL_defstash 中的 stash Foo:: 中。Bar::Baz 套件中的項目位於 Bar:: 的 stash 中的 stash Baz:: 中。

若要取得特定套件的 stash 指標,請使用函數

HV*  gv_stashpv(const char* name, I32 flags)
HV*  gv_stashsv(SV*, I32 flags)

第一個函數採用字串常數,第二個函數使用儲存在 SV 中的字串。請記住,stash 只是一個雜湊表,因此您會取得一個 HV*。如果 flags 旗標設為 GV_ADD,它將建立一個新的套件。

gv_stash*v 想要的函數名稱是您要的符號表的套件名稱。預設套件稱為 main。如果您有多個巢狀套件,請將其名稱傳遞給 gv_stash*v,並以 :: 分隔,如同 Perl 語言本身。

或者,如果您有 SV 是受祝福的參考,您可以使用下列方式找出儲存指標

HV*  SvSTASH(SvRV(SV*));

然後使用下列方式取得套件名稱本身

char*  HvNAME(HV* stash);

如果您需要祝福或重新祝福物件,可以使用下列函數

SV*  sv_bless(SV*, HV* stash)

其中第一個引數 SV* 必須是參考,而第二個引數是儲存。回傳的 SV* 現在可以像任何其他 SV 一樣使用。

有關參考和祝福的更多資訊,請參閱 perlref

I/O 處理

與 AV 和 HV 類似,IO 物件是另一種非純量 SV,可能包含輸入和輸出 PerlIO 物件或來自 opendir() 的 DIR *

您可以建立新的 IO 物件

IO*  newIO();

與其他 SV 不同,新的 IO 物件會自動祝福為 IO::File 類別。

IO 物件包含輸入和輸出 PerlIO 處理

PerlIO *IoIFP(IO *io);
PerlIO *IoOFP(IO *io);

通常,如果 IO 物件已在檔案上開啟,則輸入處理始終存在,但輸出處理僅在檔案開啟為輸出時才存在。對於檔案,如果兩個都存在,它們將是相同的 PerlIO 物件。

為 socket 和字元裝置建立不同的輸入和輸出 PerlIO 物件。

IO 物件還包含與 Perl I/O 處理相關的其他資料

 IV IoLINES(io);                /* $. */
 IV IoPAGE(io);                 /* $% */
 IV IoPAGE_LEN(io);             /* $= */
 IV IoLINES_LEFT(io);           /* $- */
 char *IoTOP_NAME(io);          /* $^ */
 GV *IoTOP_GV(io);              /* $^ */
 char *IoFMT_NAME(io);          /* $~ */
 GV *IoFMT_GV(io);              /* $~ */
 char *IoBOTTOM_NAME(io);
 GV *IoBOTTOM_GV(io);
 char IoTYPE(io);
 U8 IoFLAGS(io);

=for apidoc_sections $io_scn, $formats_section
=for apidoc_section $reports
=for apidoc Amh|IV|IoLINES|IO *io
=for apidoc Amh|IV|IoPAGE|IO *io
=for apidoc Amh|IV|IoPAGE_LEN|IO *io
=for apidoc Amh|IV|IoLINES_LEFT|IO *io
=for apidoc Amh|char *|IoTOP_NAME|IO *io
=for apidoc Amh|GV *|IoTOP_GV|IO *io
=for apidoc Amh|char *|IoFMT_NAME|IO *io
=for apidoc Amh|GV *|IoFMT_GV|IO *io
=for apidoc Amh|char *|IoBOTTOM_NAME|IO *io
=for apidoc Amh|GV *|IoBOTTOM_GV|IO *io
=for apidoc_section $io
=for apidoc Amh|char|IoTYPE|IO *io
=for apidoc Amh|U8|IoFLAGS|IO *io

其中大部分都與 格式 有關。

IoFLAGs() 可能包含多個旗標,其中最有趣的是 IOf_FLUSH ($|) 用於自動快取和 IOf_UNTAINT,可使用 IO::Handle 的 untaint() 方法 設定。

IO 物件也可能包含目錄處理

DIR *IoDIRP(io);

適用於 PerlDir_read() 等。

所有這些存取器巨集都是 l 值,沒有不同的 _set() 巨集來修改 IO 物件的成員。

雙重型態 SV

純量變數通常只包含一種數值類型,整數、雙精度浮點數、指標或參照。Perl 會自動將實際純量資料從儲存類型轉換為要求的類型。

有些純量變數包含多於一種純量資料類型。例如,變數 $! 包含 errno 的數值或從 strerrorsys_errlist[] 取得的字串等價值。

若要強制將多個資料值放入 SV,您必須執行兩件事:使用 sv_set*v 常式新增額外的純量類型,然後設定一個旗標,讓 Perl 相信它包含多於一種資料類型。設定旗標的四個巨集為

SvIOK_on
SvNOK_on
SvPOK_on
SvROK_on

您必須使用的特定巨集取決於您首先呼叫的 sv_set*v 常式。這是因為每個 sv_set*v 常式只會開啟要設定的特定資料類型的位元,並關閉所有其他位元。

例如,若要建立一個新的 Perl 變數稱為「dberror」,包含數值和描述性字串錯誤值,您可以使用下列程式碼

extern int  dberror;
extern char *dberror_list;

SV* sv = get_sv("dberror", GV_ADD);
sv_setiv(sv, (IV) dberror);
sv_setpv(sv, dberror_list[dberror]);
SvIOK_on(sv);

如果 sv_setivsv_setpv 的順序相反,則需要呼叫巨集 SvPOK_on,而不是 SvIOK_on

唯讀值

在 Perl 5.16 及更早版本中,寫入時複製(請參閱下一節)與唯讀純量共享一個旗標位元。因此,在這些版本中測試 sv_setsv 等是否會引發「修改唯讀值」錯誤的唯一方法是

SvREADONLY(sv) && !SvIsCOW(sv)

在 Perl 5.18 及更高版本中,SvREADONLY 只適用於唯讀變數,而在 5.20 中,寫入時複製的純量也可以是唯讀的,因此上述檢查是不正確的。您只需要

SvREADONLY(sv)

如果您需要經常執行此檢查,請定義您自己的巨集,如下所示

#if PERL_VERSION >= 18
# define SvTRULYREADONLY(sv) SvREADONLY(sv)
#else
# define SvTRULYREADONLY(sv) (SvREADONLY(sv) && !SvIsCOW(sv))
#endif

寫入時複製

Perl 為純量實作寫入時複製 (COW) 機制,其中字串複製並非在請求時立即進行,而是延後到其中一個或多個純量變更時才進行。這在大部分情況下是透明的,但必須小心不要修改由多個 SV 共用的字串緩衝區。

您可以使用 SvIsCOW(sv) 測試 SV 是否使用寫入時複製。

您可以呼叫 sv_force_normal(sv) 或 SvPV_force_nolen(sv) 強制 SV 建立其字串緩衝區的副本。

如果您想要讓 SV 放棄其字串緩衝區,請使用 sv_force_normal_flags(sv, SV_COW_DROP_PV) 或單純使用 sv_setsv(sv, NULL)

所有這些函式都會對唯讀純量造成 croak(有關更多資訊,請參閱前一節)。

若要測試您的程式碼是否運作正確且未修改 COW 緩衝區,在支援 mmap(2)(例如 Unix)的系統上,您可以使用 -Accflags=-DPERL_DEBUG_READONLY_COW 設定 perl,它會將緩衝區違規轉換為當機。您會發現它非常慢,因此您可能想要略過 perl 自身的測試。

魔術變數

[此節仍在建置中。請忽略這裡的所有內容。請勿張貼廣告。所有未經許可的事項均被禁止。]

任何 SV 都可能是魔術的,也就是說,它具有普通 SV 沒有的特殊功能。這些功能儲存在 SV 結構中,以 struct magic 的連結清單中,typedef 為 MAGIC

struct magic {
    MAGIC*      mg_moremagic;
    MGVTBL*     mg_virtual;
    U16         mg_private;
    char        mg_type;
    U8          mg_flags;
    I32         mg_len;
    SV*         mg_obj;
    char*       mg_ptr;
};

請注意,這是截至 patchlevel 0 的最新資訊,並且隨時可能變更。

指定魔術

Perl 使用 sv_magic 函式為 SV 新增魔術

void sv_magic(SV* sv, SV* obj, int how, const char* name, I32 namlen);

sv 參數是指向 SV 的指標,該 SV 要取得新的魔術功能。

如果 sv 尚未具備魔術,Perl 會使用 SvUPGRADE 巨集將 sv 轉換為 SVt_PVMG 類型。然後,Perl 會繼續在魔術功能的連結清單開頭新增新的魔術。任何先前同類型魔術的項目都會被刪除。請注意,這可以被覆寫,而且可以將同類型魔術的多個執行個體與 SV 關聯起來。

namenamlen 參數用於將字串與魔術關聯起來,通常是變數的名稱。namlen 儲存在 mg_len 欄位中,如果 name 為非空值,則 namesavepvn 副本或 name 本身會儲存在 mg_ptr 欄位中,具體取決於 namlen 是否分別大於零或等於零。作為特殊情況,如果 (name && namlen == HEf_SVKEY),則假設 name 包含 SV*,並以其 REFCNT 增加值的方式儲存。

sv_magic 函數使用 how 來決定是否將任何預先定義的「Magic Virtual Table」指定給 mg_virtual 欄位。請參閱下方的「Magic Virtual Tables」區段。how 參數也會儲存在 mg_type 欄位中。how 的值應從 perl.h 中找到的 PERL_MAGIC_foo 巨集組中選擇。請注意,在加入這些巨集之前,Perl 內部程式碼會直接使用字元字面值,因此您偶爾可能會看到舊程式碼或文件提到「U」魔術,而不是 PERL_MAGIC_uvar

obj 參數會儲存在 MAGIC 結構的 mg_obj 欄位中。如果它與 sv 參數不同,則 obj 物件的參考計數會遞增。如果它們相同,或如果 how 參數為 PERL_MAGIC_arylenPERL_MAGIC_regdatumPERL_MAGIC_regdata,或如果它為 NULL 指標,則僅會儲存 obj,而不會遞增參考計數。

請參閱 perlapi 中的 sv_magicext,以取得更靈活的方式將魔術加入 SV。

還有一個函數可以將魔術加入 HV

void hv_magic(HV *hv, GV *gv, int how);

這只會呼叫 sv_magic,並將 gv 參數強制轉換為 SV

若要從 SV 中移除魔術,請呼叫函數 sv_unmagic

int sv_unmagic(SV *sv, int type);

SV 最初變成魔術時,type 參數應等於 how 值。

不過,請注意 sv_unmagic 會從 SV 中移除某個 type 的所有魔術。如果您只想根據魔術虛擬表格移除某個 type 的特定魔術,請改用 sv_unmagicext

int sv_unmagicext(SV *sv, int type, MGVTBL *vtbl);

Magic Virtual Tables

MAGIC 結構中的 mg_virtual 欄位是指向 MGVTBL 的指標,MGVTBL 是函數指標的結構,代表「Magic Virtual Table」,用於處理可能套用於該變數的各種運算。

MGVTBL 有五個(或有時是八個)指標指向下列常式類型

int  (*svt_get)  (pTHX_ SV* sv, MAGIC* mg);
int  (*svt_set)  (pTHX_ SV* sv, MAGIC* mg);
U32  (*svt_len)  (pTHX_ SV* sv, MAGIC* mg);
int  (*svt_clear)(pTHX_ SV* sv, MAGIC* mg);
int  (*svt_free) (pTHX_ SV* sv, MAGIC* mg);

int  (*svt_copy) (pTHX_ SV *sv, MAGIC* mg, SV *nsv,
                                      const char *name, I32 namlen);
int  (*svt_dup)  (pTHX_ MAGIC *mg, CLONE_PARAMS *param);
int  (*svt_local)(pTHX_ SV *nsv, MAGIC *mg);

此 MGVTBL 結構會在編譯時設定在 perl.h 中,目前有 32 種類型。這些不同的結構包含指標指向各種常式,這些常式會根據呼叫的函數執行其他動作。

Function pointer    Action taken
----------------    ------------
svt_get             Do something before the value of the SV is
                    retrieved.
svt_set             Do something after the SV is assigned a value.
svt_len             Report on the SV's length.
svt_clear           Clear something the SV represents.
svt_free            Free any extra storage associated with the SV.

svt_copy            copy tied variable magic to a tied element
svt_dup             duplicate a magic structure during thread cloning
svt_local           copy magic to local value during 'local'

例如,稱為 vtbl_sv(對應於 PERL_MAGIC_svmg_type)的 MGVTBL 結構包含

{ magic_get, magic_set, magic_len, 0, 0 }

因此,當 SV 被判定為 magical 且為 PERL_MAGIC_sv 類型時,如果正在執行取得作業,則會呼叫常式 magic_get。所有各種 magical 類型的各種常式都以 magic_ 開頭。注意:magical 常式不被視為 Perl API 的一部分,且可能不會由 Perl 函式庫匯出。

最後三個槽位是最近新增的,且為了原始碼相容性,它們只會在 mg_flags 中設定三個旗標 MGf_COPYMGf_DUPMGf_LOCAL 之一的情況下進行檢查。這表示大部分程式碼可以繼續將 vtable 宣告為 5 個元素值。這三個目前專門由執行緒程式碼使用,且極有可能變更。

Magical 虛擬表的目前種類為

mg_type
(old-style char and macro)   MGVTBL         Type of magic
--------------------------   ------         -------------
\0 PERL_MAGIC_sv             vtbl_sv        Special scalar variable
#  PERL_MAGIC_arylen         vtbl_arylen    Array length ($#ary)
%  PERL_MAGIC_rhash          (none)         Extra data for restricted
                                            hashes
*  PERL_MAGIC_debugvar       vtbl_debugvar  $DB::single, signal, trace
                                            vars
.  PERL_MAGIC_pos            vtbl_pos       pos() lvalue
:  PERL_MAGIC_symtab         (none)         Extra data for symbol
                                            tables
<  PERL_MAGIC_backref        vtbl_backref   For weak ref data
@  PERL_MAGIC_arylen_p       (none)         To move arylen out of XPVAV
B  PERL_MAGIC_bm             vtbl_regexp    Boyer-Moore
                                            (fast string search)
c  PERL_MAGIC_overload_table vtbl_ovrld     Holds overload table
                                            (AMT) on stash
D  PERL_MAGIC_regdata        vtbl_regdata   Regex match position data
                                            (@+ and @- vars)
d  PERL_MAGIC_regdatum       vtbl_regdatum  Regex match position data
                                            element
E  PERL_MAGIC_env            vtbl_env       %ENV hash
e  PERL_MAGIC_envelem        vtbl_envelem   %ENV hash element
f  PERL_MAGIC_fm             vtbl_regexp    Formline
                                            ('compiled' format)
g  PERL_MAGIC_regex_global   vtbl_mglob     m//g target
H  PERL_MAGIC_hints          vtbl_hints     %^H hash
h  PERL_MAGIC_hintselem      vtbl_hintselem %^H hash element
I  PERL_MAGIC_isa            vtbl_isa       @ISA array
i  PERL_MAGIC_isaelem        vtbl_isaelem   @ISA array element
k  PERL_MAGIC_nkeys          vtbl_nkeys     scalar(keys()) lvalue
L  PERL_MAGIC_dbfile         (none)         Debugger %_<filename
l  PERL_MAGIC_dbline         vtbl_dbline    Debugger %_<filename
                                            element
N  PERL_MAGIC_shared         (none)         Shared between threads
n  PERL_MAGIC_shared_scalar  (none)         Shared between threads
o  PERL_MAGIC_collxfrm       vtbl_collxfrm  Locale transformation
P  PERL_MAGIC_tied           vtbl_pack      Tied array or hash
p  PERL_MAGIC_tiedelem       vtbl_packelem  Tied array or hash element
q  PERL_MAGIC_tiedscalar     vtbl_packelem  Tied scalar or handle
r  PERL_MAGIC_qr             vtbl_regexp    Precompiled qr// regex
S  PERL_MAGIC_sig            vtbl_sig       %SIG hash
s  PERL_MAGIC_sigelem        vtbl_sigelem   %SIG hash element
t  PERL_MAGIC_taint          vtbl_taint     Taintedness
U  PERL_MAGIC_uvar           vtbl_uvar      Available for use by
                                            extensions
u  PERL_MAGIC_uvar_elem      (none)         Reserved for use by
                                            extensions
V  PERL_MAGIC_vstring        (none)         SV was vstring literal
v  PERL_MAGIC_vec            vtbl_vec       vec() lvalue
w  PERL_MAGIC_utf8           vtbl_utf8      Cached UTF-8 information
X  PERL_MAGIC_destruct       vtbl_destruct  destruct callback
x  PERL_MAGIC_substr         vtbl_substr    substr() lvalue
Y  PERL_MAGIC_nonelem        vtbl_nonelem   Array element that does not
                                            exist
y  PERL_MAGIC_defelem        vtbl_defelem   Shadow "foreach" iterator
                                            variable / smart parameter
                                            vivification
Z  PERL_MAGIC_hook           vtbl_hook      %{^HOOK} hash
z  PERL_MAGIC_hookelem       vtbl_hookelem  %{^HOOK} hash element
\  PERL_MAGIC_lvref          vtbl_lvref     Lvalue reference
                                            constructor
]  PERL_MAGIC_checkcall      vtbl_checkcall Inlining/mutation of call
                                            to this CV
^  PERL_MAGIC_extvalue       (none)         Value magic available for
                                            use by extensions
~  PERL_MAGIC_ext            (none)         Variable magic available
                                            for use by extensions

當表格中同時存在大寫和小寫字母時,大寫字母通常用於表示某種複合類型(清單或雜湊),而小寫字母則用於表示該複合類型的元素。有些內部程式碼會使用這種大小寫關係。但是,'v' 和 'V'(向量和 v 字串)完全無關。

PERL_MAGIC_extPERL_MAGIC_extvaluePERL_MAGIC_uvar magical 類型特別定義供擴充套件使用,且 perl 本身不會使用。擴充套件可以使用 PERL_MAGIC_extPERL_MAGIC_extvalue magical 來「附加」私密資訊至變數(通常為物件)。這特別有用,因為一般 perl 程式碼無法損毀此私密資訊(與使用雜湊物件的額外元素不同)。PERL_MAGIC_extvalue 是值 magical(與 PERL_MAGIC_extPERL_MAGIC_uvar 不同),表示在區域化時,新值不會是 magical。

類似地,PERL_MAGIC_uvar magical 可以很像 tie(),在每次使用或變更純量的值時呼叫 C 函式。MAGICmg_ptr 欄位指向 ufuncs 結構

struct ufuncs {
    I32 (*uf_val)(pTHX_ IV, SV*);
    I32 (*uf_set)(pTHX_ IV, SV*);
    IV uf_index;
};

當從 SV 讀取或寫入 SV 時,uf_valuf_set 函式會以 uf_index 為第一個引數,並以指向 SV 的指標為第二個引數來呼叫。以下是新增 PERL_MAGIC_uvar magical 的簡單範例。請注意,ufuncs 結構會由 sv_magic 複製,因此您可以安全地將其配置在堆疊中。

void
Umagic(sv)
    SV *sv;
PREINIT:
    struct ufuncs uf;
CODE:
    uf.uf_val   = &my_get_fn;
    uf.uf_set   = &my_set_fn;
    uf.uf_index = 0;
    sv_magic(sv, 0, PERL_MAGIC_uvar, (char*)&uf, sizeof(uf));

附加 PERL_MAGIC_uvar 至陣列是允許的,但沒有作用。

對於雜湊,有一個專門的掛鉤,用於控制雜湊鍵(但不是值)。如果 ufuncs 結構中的「set」函數為 NULL,這個掛鉤會呼叫 PERL_MAGIC_uvar 的「get」魔法。每當雜湊透過函數 hv_store_enthv_fetch_enthv_delete_enthv_exists_ent 使用指定為 SV 的鍵存取時,就會啟動這個掛鉤。透過沒有 ..._ent 後綴的函數將鍵作為字串存取,會繞過這個掛鉤。請參閱 Hash::Util::FieldHash 中的「GUTS」 以取得詳細說明。

請注意,由於多個擴充功能可能使用 PERL_MAGIC_extPERL_MAGIC_uvar 魔法,因此擴充功能必須特別小心以避免衝突。通常只對祝福為與擴充功能相同類別的物件使用魔法就已足夠。對於 PERL_MAGIC_ext 魔法,通常建議定義一個 MGVTBL,即使其所有欄位都為 0,這樣才能使用其魔法虛擬表格將個別 MAGIC 指標識別為特定類型的魔法。mg_findext 提供了一個簡單的方法來執行此操作

STATIC MGVTBL my_vtbl = { 0, 0, 0, 0, 0, 0, 0, 0 };

MAGIC *mg;
if ((mg = mg_findext(sv, PERL_MAGIC_ext, &my_vtbl))) {
    /* this is really ours, not another module's PERL_MAGIC_ext */
    my_priv_data_t *priv = (my_priv_data_t *)mg->mg_ptr;
    ...
}

另請注意,前面說明的 sv_set*()sv_cat*() 函數不會對其目標呼叫「設定」魔法。使用者必須在呼叫這些函數後呼叫 SvSETMAGIC() 巨集,或使用 sv_set*_mg()sv_cat*_mg() 函數之一來執行此操作。類似地,一般 C 程式碼必須呼叫 SvGETMAGIC() 巨集,才能在函數中呼叫任何「取得」魔法,這些函數在函數中使用從外部來源取得的 SV,而且不會處理魔法。請參閱 perlapi 以取得這些函數的說明。例如,呼叫 sv_cat*() 函數通常需要後接 SvSETMAGIC(),但不需要先前的 SvGETMAGIC(),因為其實作會處理「取得」魔法。

尋找魔法

MAGIC *mg_find(SV *sv, int type); /* Finds the magic pointer of that
                                   * type */

此常式傳回儲存在 SV 中的 MAGIC 結構指標。如果 SV 沒有該魔法功能,則傳回 NULL。如果 SV 有該魔法功能的多個執行個體,則會傳回第一個執行個體。mg_findext 可用於根據魔法類型和魔法虛擬表格尋找 SV 的 MAGIC 結構

MAGIC *mg_findext(SV *sv, int type, MGVTBL *vtbl);

此外,如果傳遞給 mg_findmg_findext 的 SV 不是 SVt_PVMG 類型,Perl 可能會核心傾印。

int mg_copy(SV* sv, SV* nsv, const char* key, STRLEN klen);

此常式會檢查 sv 有哪些類型的魔法。如果 mg_type 欄位是大寫字母,則 mg_obj 會複製到 nsv,但 mg_type 欄位會變更為小寫字母。

了解繫結雜湊和陣列的魔法

繫結雜湊和陣列是 PERL_MAGIC_tied 魔法類型的魔法野獸。

警告:自 5.004 版本開始,正確使用陣列和雜湊存取函式需要了解一些注意事項。其中一些注意事項實際上被視為 API 中的錯誤,將在後續版本中修正,並在下方以 [MAYCHANGE] 標示。如果你發現自己實際上套用本節中的此類資訊,請注意行為可能會在未來變更,嗯,恕不另行通知。

perl tie 函式會將變數與實作各種 GET、SET 等方法的物件關聯起來。若要從 XSUB 執行等同於 perl tie 函式,你必須模擬此行為。下方的程式碼執行必要的步驟,首先建立一個新的雜湊,然後建立第二個雜湊,並將其祝福成將實作 tie 方法的類別。最後將兩個雜湊繫結在一起,並傳回已繫結雜湊的參考。請注意,下方的程式碼並未在 MyTie 類別中呼叫 TIEHASH 方法,有關如何執行此動作的詳細資訊,請參閱 "Calling Perl Routines from within C Programs"

SV*
mytie()
PREINIT:
    HV *hash;
    HV *stash;
    SV *tie;
CODE:
    hash = newHV();
    tie = newRV_noinc((SV*)newHV());
    stash = gv_stashpv("MyTie", GV_ADD);
    sv_bless(tie, stash);
    hv_magic(hash, (GV*)tie, PERL_MAGIC_tied);
    RETVAL = newRV_noinc(hash);
OUTPUT:
    RETVAL

當給予繫結陣列引數時,av_store 函式僅將陣列的魔法複製到要「儲存」的值上,使用 mg_copy。它也可能會傳回 NULL,表示實際上不需要將值儲存在陣列中。[MAYCHANGE] 在繫結陣列上呼叫 av_store 之後,呼叫者通常需要呼叫 mg_set(val) 來實際呼叫 TIEARRAY 物件上的 perl 層級「STORE」方法。如果 av_store 確實傳回 NULL,通常也需要呼叫 SvREFCNT_dec(val) 來避免記憶體外洩。[/MAYCHANGE]

前一段文字也適用於使用 hv_storehv_store_ent 函式的繫結雜湊存取。

av_fetch 和對應的雜湊函式 hv_fetchhv_fetch_ent 實際上傳回一個未定義的 mortal 值,其魔法已使用 mg_copy 初始化。請注意,如此傳回的值不需要取消配置,因為它已經是 mortal。[MAYCHANGE] 但是你將需要對傳回的值呼叫 mg_get(),才能實際呼叫底層 TIE 物件上的 perl 層級「FETCH」方法。類似地,你也可以在傳回值上呼叫 mg_set(),並使用 sv_setsv 為其指定適當的值,這將呼叫 TIE 物件上的「STORE」方法。[/MAYCHANGE]

[MAYCHANGE]換句話說,對於繫結陣列和雜湊,陣列或雜湊的提取/儲存函數並未真正提取和儲存實際值。它們僅呼叫mg_copy,將魔法附加到「儲存」或「提取」的值。後續對mg_getmg_set的呼叫實際上執行在底層物件上呼叫 TIE 方法的工作。因此,魔法機制目前實作了一種對陣列和雜湊的延遲存取。

目前(perl 版本 5.004),使用雜湊和陣列存取函數需要使用者知道他們是在操作「一般」雜湊和陣列,還是它們的繫結變體。API 可能會在未來版本中變更,以提供對繫結和一般資料類型的更透明存取。[/MAYCHANGE]

您最好了解 TIEARRAY 和 TIEHASH 介面只是在使用一致的雜湊和陣列語法時呼叫一些 perl 方法呼叫的糖衣。使用此糖衣會造成一些額外負擔(通常每個 FETCH/STORE 操作額外約兩個到四個 opcode,除了建立呼叫這些方法所需的所有 mortal 變數)。如果 TIE 方法本身很龐大,這個額外負擔會相對較小,但如果它們只有幾行陳述式長,額外負擔就不會微不足道。

在地化變更

Perl 有個非常方便的建構

{
  local $var = 2;
  ...
}

此建構大約等於

{
  my $oldvar = $var;
  $var = 2;
  ...
  $var = $oldvar;
}

最大的差異在於,第一個建構會恢復 $var 的初始值,而不管控制如何退出區塊:gotoreturndie/eval 等。它也稍微更有效率。

透過 Perl API,有一種方法可以從 C 達成類似的任務:建立一個偽區塊,並安排在區塊結束時自動復原某些變更,無論是明確的或透過非區域離開(透過 die())。一個區塊類型的建構是由一對 ENTER/LEAVE 巨集建立的(請參閱 "perlcall 中的「傳回一個純量」)。此類建構可以特別為某些重要的區域任務建立,或使用現有的建構(例如封裝 Perl 子常式/區塊的邊界,或用於釋放 TMP 的現有配對)。(在第二種情況下,額外區域化的開銷幾乎可以忽略不計。)請注意,任何 XSUB 都會自動封裝在 ENTER/LEAVE 配對中。

在這種偽區塊中,可以使用下列服務

SAVEINT(int i)
SAVEIV(IV i)
SAVEI32(I32 i)
SAVELONG(long i)
SAVEI8(I8 i)
SAVEI16(I16 i)
SAVEBOOL(int i)
SAVESTRLEN(STRLEN i)

這些巨集會安排在封裝偽區塊結束時復原整數變數 i 的值。

SAVESPTR(s)
SAVEPPTR(p)

這些巨集會安排在復原指標 sp 的值。s 必須是轉換為 SV* 及轉回後仍存在的類型指標,p 應能轉換為 char* 及轉回後仍存在。

SAVERCPV(char **ppv)

此巨集會安排在目前的偽區塊完成時,將透過呼叫 rcpv_new() 分配的 char * 變數值還原至其先前狀態。呼叫時儲存在 *ppv 中的指標會增加參考計數並儲存在儲存堆疊中。稍後當目前的偽區塊完成時,儲存在 *ppv 中的值會減少參考計數,並從儲存堆疊中還原先前的值,而儲存堆疊中的值也會減少參考計數。

這是 SAVEGENERICSV()RCPV 等效項。

SAVEGENERICSV(SV **psv)

此巨集會安排在目前的偽區塊完成時,將 SV * 變數值還原至其先前狀態。呼叫時儲存在 *psv 中的指標會增加參考計數並儲存在儲存堆疊中。稍後當目前的偽區塊完成時,儲存在 *ppv 中的值會減少參考計數,並從儲存堆疊中還原先前的值,而儲存堆疊中的值也會減少參考計數。這是 local $sv 的 C 等效項。

SAVEFREESV(SV *sv)

偽區塊 結束時會減少 sv 的參考計數。這類似於 sv_2mortal,因為它也是一種執行延遲的 SvREFCNT_dec 的機制。然而,sv_2mortalsv 的生命週期延長至下一個陳述式的開始,而 SAVEFREESV 則將其延長至封閉範圍的結束。這些生命週期可能大不相同。

另請比較 SAVEMORTALIZESV

SAVEMORTALIZESV(SV *sv)

就像 SAVEFREESV 一樣,但在當前範圍結束時讓 sv 變成 mortal,而不是減少其參考計數。這通常會讓 sv 保持運作狀態,直到呼叫目前運作中範圍的陳述式執行完畢。

SAVEFREEOP(OP *op)

偽區塊 結束時會對 OP * 執行 op_free()

SAVEFREEPV(p)

p 指向的記憶體區塊會在目前的 偽區塊 結尾處被 Safefree() 釋放。

SAVEFREERCPV(char *pv)

確保由 rcpv_new() 呼叫建立的 char * 會在目前的 偽區塊 結尾處被 rcpv_free() 釋放。

這是 SAVEFREESV() 的 RCPV 等效函式。

SAVECLEARSV(SV *sv)

清除 偽區塊 結尾處與 sv 對應的暫存區中的一個槽位。

SAVEDELETE(HV *hv, char *key, I32 length)

hv 的鍵值 key 會在 偽區塊 結尾處被刪除。由 key 指向的字串會被 Safefree() 釋放。如果在短期儲存區中有一個 key,對應的字串可能會像這樣重新配置

SAVEDELETE(PL_defstash, savepv(tmpbuf), strlen(tmpbuf));
SAVEDESTRUCTOR(DESTRUCTORFUNC_NOCONTEXT_t f, void *p)

偽區塊 結尾處,函式 f 會以唯一的引數 p 呼叫,而 p 可能為 NULL。

SAVEDESTRUCTOR_X(DESTRUCTORFUNC_t f, void *p)

偽區塊 結尾處,函式 f 會以隱含的內容引數(如果有的話)和 p 呼叫,而 p 可能為 NULL。

請注意,目前的偽區塊結尾 可能會在 目前的陳述式結尾 之後很久才發生。您可能希望改看 MORTALDESTRUCTOR_X() 巨集。

MORTALSVFUNC_X(SVFUNC_t f, SV *sv)

目前的陳述式 結尾處,函式 f 會以隱含的內容引數(如果有的話)和 sv 呼叫,而 sv 可能為 NULL。

請注意,毀損函式的參數引數與相關的 SAVEDESTRUCTOR_X() 不同,在於它必定是 NULL 或 SV*

請注意,目前的陳述式結尾 可能會在 目前的偽區塊結尾 之前很久才發生。您可能希望改看 SAVEDESTRUCTOR_X() 巨集。

MORTALDESTRUCTOR_SV(SV *coderef, SV *args)

目前陳述的結尾,coderef 中包含的 Perl 函數會呼叫 args 中提供的參數(如果有)。請參閱 mortal_destructor_sv() 的文件,以取得有關 args 參數處理方式的詳細資訊。

請注意,目前陳述的結尾可能會在目前偽區塊的結尾之前發生。如果您希望在目前偽區塊的結尾呼叫 Perl 函數,則應改用 SAVEDESTRUCTOR_X() API,這將需要您建立一個 C 封裝函數來呼叫 Perl 函數。

SAVESTACK_POS()

Perl 內部堆疊(請參閱 SP)的目前偏移量會在偽區塊的結尾復原。

下列 API 清單包含函數,因此需要明確提供可修改資料的指標(C 指標或 Perlish GV *)。在上述巨集使用 int 的地方,類似的函數會使用 int *

上述的其他巨集有函數實作,但最好只使用巨集,而不是這些或下列巨集。

SV* save_scalar(GV *gv)

等同於 Perl 程式碼 local $gv

AV* save_ary(GV *gv)
HV* save_hash(GV *gv)

類似於 save_scalar,但會將 @gv%gv 本地化。

void save_item(SV *item)

複製 SV 的目前值。在離開目前的 ENTER/LEAVE 偽區塊 時,SV 的值會使用儲存的值復原。它不處理魔法。如果會影響魔法,請使用 save_scalar

SV* save_svref(SV **sptr)

類似於 save_scalar,但會重新建立一個 SV *

void save_aptr(AV **aptr)
void save_hptr(HV **hptr)

類似於 save_svref,但將 AV *HV * 本地化。

Alias 模組實作了 呼叫者範圍 內基本類型的本地化。有興趣了解如何在包含範圍內本地化項目的人員也應該參考此處。

子常式

XSUB 和引數堆疊

XSUB 機制是 Perl 程式存取 C 子常式的簡單方式。XSUB 常式將會有一個堆疊,其中包含來自 Perl 程式的引數,以及將 Perl 資料結構對應到 C 等效項的方式。

堆疊引數可透過 ST(n) 巨集存取,它會傳回第 n 個堆疊引數。引數 0 是在 Perl 子常式呼叫中傳入的第一個引數。這些引數是 SV*,而且可以在使用 SV* 的任何地方使用。

在大部分情況下,C 常式的輸出都可以透過使用 RETVAL 和 OUTPUT 指令來處理。不過,在某些情況下,引數堆疊還不夠長,無法處理所有傳回值。一個範例是 POSIX tzname() 呼叫,它不接受任何引數,但會傳回兩個引數,即當地時區的標準時間和夏令時間縮寫。

若要處理這種情況,請使用 PPCODE 指令,並使用巨集延伸堆疊

EXTEND(SP, num);

其中 SP 是表示堆疊指標本機拷貝的巨集,而 num 是堆疊應該延伸的元素數目。

現在堆疊上有空間了,可以使用 PUSHs 巨集將值推入堆疊。推入的值通常需要是「暫存的」(請參閱 "參考計數和暫存性")

PUSHs(sv_2mortal(newSViv(an_integer)))
PUSHs(sv_2mortal(newSVuv(an_unsigned_integer)))
PUSHs(sv_2mortal(newSVnv(a_double)))
PUSHs(sv_2mortal(newSVpv("Some String",0)))
/* Although the last example is better written as the more
 * efficient: */
PUSHs(newSVpvs_flags("Some String", SVs_TEMP))

現在呼叫 tzname 的 Perl 程式會將兩個值指定為

($standard_abbrev, $summer_abbrev) = POSIX::tzname;

將值推入堆疊的另一種 (而且可能更簡單的) 方法是使用巨集

XPUSHs(SV*)

如果需要,這個巨集會自動調整堆疊。因此,您不需要呼叫 EXTEND 來延伸堆疊。

儘管本文件早期版本中建議使用巨集 (X)PUSH[iunp],但這些巨集適用於傳回多個結果的 XSUB。對於這種情況,請堅持使用上面所示的 (X)PUSHs 巨集,或改用新的 m(X)PUSH[iunp] 巨集;請參閱 "將 C 值放入 Perl 堆疊"

如需更多資訊,請參閱 perlxsperlxstut

使用 XSUB 自動載入

如果 AUTOLOAD 常式程式是 XSUB,如同 Perl 子常式程式,Perl 會將自動載入子常式程式的完全限定名稱放入 XSUB 套件的 $AUTOLOAD 變數中。

不過,它也會將相同資訊放入 XSUB 本身的特定欄位中

HV *stash           = CvSTASH(cv);
const char *subname = SvPVX(cv);
STRLEN name_length  = SvCUR(cv); /* in bytes */
U32 is_utf8         = SvUTF8(cv);

SvPVX(cv) 只包含子名稱本身,不包括套件。對於 UNIVERSAL 或其超類別中的 AUTOLOAD 常式程式,CvSTASH(cv) 會在不存在的套件上進行方法呼叫時傳回 NULL。

注意:設定 $AUTOLOAD 已在 5.6.1 中停止運作,它完全不支援 XS AUTOLOAD 子常式程式。Perl 5.8.0 引入了在 XSUB 本身中使用欄位的做法。Perl 5.16.0 還原了 $AUTOLOAD 的設定。如果您需要支援 5.8-5.14,請使用 XSUB 的欄位。

從 C 程式中呼叫 Perl 常式程式

有四個常式程式可用於從 C 程式中呼叫 Perl 子常式程式。這四個常式程式是

I32  call_sv(SV*, I32);
I32  call_pv(const char*, I32);
I32  call_method(const char*, I32);
I32  call_argv(const char*, I32, char**);

最常使用的常式程式是 call_svSV* 參數包含要呼叫的 Perl 子常式程式的名稱,或對子常式程式的參照。第二個參數包含控制子常式程式呼叫內容的旗標,例如子常式程式是否傳遞參數、如何擷取錯誤,以及如何處理回傳值。

這四個常式程式都會傳回子常式程式在 Perl 堆疊中傳回的參數數目。

在 Perl v5.6.0 之前,這些常式程式會稱為 perl_call_sv 等,但這些名稱現在已不建議使用;已提供同名的巨集以確保相容性。

在使用其中任何常式程式(call_argv 除外)時,程式設計師必須操作 Perl 堆疊。其中包括以下巨集和函式

dSP
SP
PUSHMARK()
PUTBACK
SPAGAIN
ENTER
SAVETMPS
FREETMPS
LEAVE
XPUSH*()
POP*()

有關從 C 呼叫 Perl 的慣例的詳細說明,請參閱 perlcall

將 C 值放入 Perl 堆疊

許多 opcode(這是 Perl 內部堆疊機器中的基本操作)會將 SV* 放入堆疊中。然而,作為最佳化,對應的 SV(通常)不會每次都重新建立。opcode 會重複使用特別指定的 SV(目標),而這些 SV(作為推論)不會持續地釋放/建立。

每個目標只建立一次(但請參閱下方的「Scratchpad 和遞迴」),當 opcode 需要將整數、雙精度浮點數或字串放入堆疊中時,它只會設定其目標的對應部分,並將目標放入堆疊中。

將此目標放入堆疊中的巨集為 PUSHTARG,它會直接用於某些 opcode 中,以及間接用於其他數百萬個 opcode 中,這些 opcode 會透過 (X)PUSH[iunp] 使用它。

由於目標會重複使用,因此在將多個值推入堆疊時,您必須小心。下列程式碼不會執行您認為它會執行的動作

XPUSHi(10);
XPUSHi(20);

這會轉譯為「將 TARG 設定為 10,將指向 TARG 的指標推入堆疊中;將 TARG 設定為 20,將指向 TARG 的指標推入堆疊中」。在操作結束時,堆疊中不包含值 10 和 20,但實際上包含兩個指向 TARG 的指標,而我們已將其設定為 20。

如果您需要推入多個不同的值,則您應該使用 (X)PUSHs 巨集,或使用新的 m(X)PUSH[iunp] 巨集,這些巨集都不會使用 TARG(X)PUSHs 巨集只會將 SV* 推入堆疊中,正如在「XSUB 和引數堆疊」下方所述,這通常需要是「暫存的」。新的 m(X)PUSH[iunp] 巨集透過為您建立新的暫存(透過 (X)PUSHmortal)來讓這變得更容易達成,將其推入堆疊中(在 mXPUSH[iunp] 巨集的情況下,必要時會延伸堆疊),然後設定其值。因此,與其撰寫以下內容來「修正」上述範例

XPUSHs(sv_2mortal(newSViv(10)))
XPUSHs(sv_2mortal(newSViv(20)))

您可以簡單地撰寫

mXPUSHi(10)
mXPUSHi(20)

在相關的備註中,如果您確實使用 (X)PUSH[iunp],則您需要在變數宣告中使用 dTARG,以便 *PUSH* 巨集可以使用區域變數 TARG。另請參閱 dTARGETdXSTARG

Scratchpad

問題仍然在於作為 opcode 目標 的 SV 何時建立。答案是在編譯目前的單元(子常式或檔案,針對子常式外部陳述式的 opcode)時建立。這段期間會建立一個特殊的匿名 Perl 陣列,稱為目前單元的暫存區。

暫存區會保留目前單元的字彙 SV,並作為 opcode 的目標。本文件的前一版本指出,可以透過查看其旗標來推斷 SV 是否存在於暫存區:字彙具有設定的 SVs_PADMY,而目標具有設定的 SVs_PADTMP。但這從未完全正確。SVs_PADMY 可以設定在不再駐留在任何暫存區的變數上。雖然目標具有設定的 SVs_PADTMP,但它也可以設定在從未駐留在暫存區,但仍像目標一樣運作的變數上。從 perl 5.21.5 開始,SVs_PADMY 旗標不再使用,並定義為 0。SvPADMY() 現在會傳回沒有 SVs_PADTMP 的任何項目的 true。

OP 與目標之間的對應並非一對一。如果這不會與暫存變數預期的生命週期衝突,單元的編譯樹中不同的 OP 可以使用相同的目標。

暫存區和遞迴

事實上,已編譯單元包含指向暫存區 AV 的指標並非 100% 正確。事實上,它包含指向 (最初) 一個元素的 AV 的指標,而這個元素是暫存區 AV。我們為什麼需要額外的間接層級?

答案是遞迴,也許是執行緒。這兩者都可以建立多個執行指標進入同一個子常式。為了讓子常式不會覆寫父常式的暫存變數(其生命週期涵蓋對子常式的呼叫),父常式和子常式應該有不同的暫存區。(而且字彙應該無論如何都是分開的!)

因此,每個子常式都會產生一個暫存區陣列(長度為 1)。每次進入子常式時,都會檢查遞迴的目前深度是否不超過這個陣列的長度,如果是,就會建立新的暫存區並推入陣列中。

這個暫存區上的目標undef,但它們已經標記有正確的旗標。

記憶體配置

配置

所有要與 Perl API 函數一起使用的記憶體都應該使用本節中所述的巨集來操作。這些巨集提供了 perl 內部所使用的實際 malloc 實作之間必要的透明度。

以下三個巨集用於最初配置記憶體

Newx(pointer, number, type);
Newxc(pointer, number, type, cast);
Newxz(pointer, number, type);

第一個引數 指標 應該是會指向新配置記憶體的變數名稱。

第二個和第三個引數 數量類型 指定應該配置多少個指定類型資料結構。類型 引數會傳遞給 sizeofNewxc 的最後一個引數 轉型 應該在 指標 引數與 類型 引數不同時使用。

NewxNewxc 巨集不同,Newxz 巨集會呼叫 memzero 將所有新配置的記憶體歸零。

重新配置

Renew(pointer, number, type);
Renewc(pointer, number, type, cast);
Safefree(pointer)

這三個巨集用於變更記憶體緩衝區大小或釋放不再需要的記憶體。RenewRenewc 的引數與 NewNewc 相符,但不需要「magic cookie」引數。

移動

Move(source, dest, number, type);
Copy(source, dest, number, type);
Zero(dest, number, type);

這三個巨集用於移動、複製或將先前配置的記憶體歸零。來源目的地 引數指向來源和目的地的起始點。Perl 會移動、複製或將 類型 資料結構大小的 數量 個執行個體歸零(使用 sizeof 函數)。

PerlIO

Perl 的最新開發版本一直在嘗試移除 Perl 對「一般」標準 I/O 組合的依賴性,並允許使用其他 stdio 實作。這包括建立一個新的抽象層,然後呼叫 Perl 編譯時所使用的 stdio 實作。所有 XSUB 現在都應該使用 PerlIO 抽象層中的函數,而不對所使用的 stdio 類型做任何假設。

如需 PerlIO 抽象的完整說明,請參閱 perlapio

編譯程式碼

程式碼樹

我們在此說明 Perl 將您的程式碼轉換成的內部形式。從一個簡單的範例開始

$a = $b + $c;

這會轉換成類似這棵樹的結構

       assign-to
     /           \
    +             $a
  /   \
$b     $c

(但稍微複雜一點)。這棵樹反映了 Perl 解析您程式碼的方式,但與執行順序無關。有一個額外的「執行緒」通過樹的節點,顯示節點的執行順序。在我們上述的簡化範例中,它看起來像

$b ---> $c ---> + ---> $a ---> assign-to

但對於 $a = $b + $c 的實際編譯樹而言,它是不同的:一些節點會「最佳化」。因此,儘管實際的樹包含比我們的簡化範例更多的節點,但執行順序與我們的範例相同。

檢查樹

如果您已編譯 perl 以進行除錯(通常在 Configure 命令列上使用 -DDEBUGGING),您可以透過在 Perl 命令列上指定 -Dx 來檢查編譯樹。輸出會針對每個節點佔用多行,而對於 $b+$c,它看起來像這樣

5           TYPE = add  ===> 6
            TARG = 1
            FLAGS = (SCALAR,KIDS)
            {
                TYPE = null  ===> (4)
                  (was rv2sv)
                FLAGS = (SCALAR,KIDS)
                {
3                   TYPE = gvsv  ===> 4
                    FLAGS = (SCALAR)
                    GV = main::b
                }
            }
            {
                TYPE = null  ===> (5)
                  (was rv2sv)
                FLAGS = (SCALAR,KIDS)
                {
4                   TYPE = gvsv  ===> 5
                    FLAGS = (SCALAR)
                    GV = main::c
                }
            }

這棵樹有 5 個節點(每個 TYPE 規格符一個),其中只有 3 個未最佳化(左欄中的每個數字一個)。給定節點的直接子節點對應於相同縮排層級上的 {} 對,因此此清單對應於樹

    add
  /     \
null    null
 |       |
gvsv    gvsv

執行順序由 ===> 標記表示,因此它是 3 4 5 6(節點 6 未包含在上述清單中),即 gvsv gvsv add whatever

這些節點中的每個都表示一個 op,這是 Perl 核心中的基本運算。實作每個運算的程式碼可以在 pp*.c 檔案中找到;實作類型為 gvsv 的 op 的函式是 pp_gvsv,依此類推。如上方的樹所示,不同的 op 有不同的子節點數目:add 是二元運算子,正如預期的那樣,因此有兩個子節點。為了容納不同數目的子節點,有各種類型的 op 資料結構,它們以不同的方式連結在一起。

最簡單的 op 結構類型是 OP:它沒有子節點。一元運算子 UNOP 有一個子節點,它由 op_first 欄位指向。二元運算子 (BINOP) 不僅有 op_first 欄位,還有一個 op_last 欄位。最複雜的 op 類型是 LISTOP,它可以有任意數目的子節點。在這種情況下,第一個子節點由 op_first 指向,最後一個子節點由 op_last 指向。中間的子節點可以透過從第一個子節點到最後一個子節點反覆追蹤 OpSIBLING 指標來找到(但請參閱下方)。

還有一些其他 op 類型:PMOP 儲存正規表示式,沒有子節點,而 LOOP 可能有或沒有子節點。如果 op_children 欄位非零,它會像 LISTOP 一樣運作。更複雜的是,如果 UNOP 在最佳化後實際上是 null op(請參閱「編譯步驟 2:內容傳播」),它仍然會根據其前類型擁有子節點。

最後,還有一個 LOGOP,或邏輯 op。它就像 LISTOP 一樣,有一個或多個子節點,但它沒有 op_last 欄位:因此你必須先追蹤 op_first,然後再追蹤 OpSIBLING 鏈本身才能找到最後一個子節點。它有一個 op_other 欄位,這與下面描述的 op_next 欄位類似,表示替代執行路徑。andor? 等運算子是 LOGOP。請注意,一般來說,op_other 可能不會指向 LOGOP 的任何直接子節點。

從版本 5.21.2 開始,使用實驗定義 -DPERL_OP_PARENT 建置的 perl 會為每個 op 新增一個額外的布林旗標 op_moresib。當未設定時,這表示這是 OpSIBLING 鏈中的最後一個 op。這會釋放最後一個同層節點的 op_sibling 欄位,以指向父節點 op。在此建置下,該欄位也會重新命名為 op_sibparent,以反映其共同角色。巨集 OpSIBLING(o) 包含此特殊行為,並始終在最後一個同層節點傳回 NULL。使用此建置,op_parent(o) 函數可用於尋找任何 op 的父節點。因此,為了向前相容,你應該始終使用 OpSIBLING(o) 巨集,而不是直接存取 op_sibling

檢查樹狀結構的另一種方法是使用編譯器後端模組,例如 B::Concise

編譯步驟 1:檢查常式

樹狀結構由編譯器建立,而 yacc 程式碼則提供它辨識到的結構。由於 yacc 是由下而上運作,因此 perl 編譯的第一個步驟也是如此。

對於 perl 開發人員來說,這個步驟之所以有趣,是因為可以在此步驟執行一些最佳化。這是由所謂的「檢查常式」進行最佳化。節點名稱與對應檢查常式之間的對應關係說明於 opcode.pl(如果您修改此檔案,請不要忘記執行 make regen_headers)。

當節點完全建構完成,但執行順序執行緒除外,就會呼叫檢查常式。由於此時沒有任何回溯連結到目前建構的節點,因此可以對頂層節點執行大多數任何操作,包括釋放它和/或在它上面/下面建立新的節點。

檢查常式會傳回應該插入樹狀結構的節點(如果頂層節點沒有修改,檢查常式會傳回其引數)。

根據慣例,檢查常式命名為 ck_*。它們通常會從 new*OP 子常式(或 convert)呼叫(而這些子常式又會從 perly.y 呼叫)。

編譯步驟 1a:常數摺疊

在呼叫檢查常式之後,會立即檢查傳回的節點是否為編譯時期可執行。如果是(值判斷為常數),則會立即執行它,並以對應子樹的「傳回值」取代一個 constant 節點。子樹會被刪除。

如果沒有執行常數摺疊,則會建立執行順序執行緒。

編譯步驟 2:內容傳播

當編譯樹的一部分的內容已知時,它會透過樹狀結構向下傳播。此時,內容可以有 5 個值(而不是執行時期內容的 2 個值):void、boolean、scalar、list 和 lvalue。與步驟 1 相比,此步驟會從上到下處理:節點的內容會決定其子節點的內容。

此時會執行其他與內容相關的最佳化。由於編譯樹在此刻包含回溯參考(透過「執行緒」指標),因此無法立即釋放節點。為了允許在此階段最佳化移除節點,這些節點會被 null() 化,而不是釋放(也就是說,它們的類型會變更為 OP_NULL)。

編譯步驟 3:窺孔最佳化

建立子常式(或 eval 或檔案)的編譯樹後,會對程式碼執行額外的步驟。此步驟既非由上而下,也非由下而上,而是依執行順序(條件式有額外的複雜性)。在此階段執行的最佳化受限於步驟 2 中的相同限制。

窺孔最佳化透過呼叫由全域變數 PL_peepp 指向的函式來完成。預設情況下,PL_peepp 只會呼叫由全域變數 PL_rpeepp 指向的函式。預設情況下,此函式會執行一些基本運算修正和最佳化,並針對每個運算的側鏈(由條件式產生)遞迴呼叫 PL_rpeepp。延伸模組可以提供額外的最佳化或修正,並掛接到每個子常式或遞迴階段,如下所示

static peep_t prev_peepp;
static void my_peep(pTHX_ OP *o)
{
    /* custom per-subroutine optimisation goes here */
    prev_peepp(aTHX_ o);
    /* custom per-subroutine optimisation may also go here */
}
BOOT:
    prev_peepp = PL_peepp;
    PL_peepp = my_peep;

static peep_t prev_rpeepp;
static void my_rpeep(pTHX_ OP *first)
{
    OP *o = first, *t = first;
    for(; o = o->op_next, t = t->op_next) {
        /* custom per-op optimisation goes here */
        o = o->op_next;
        if (!o || o == t) break;
        /* custom per-op optimisation goes AND here */
    }
    prev_rpeepp(aTHX_ orig_o);
}
BOOT:
    prev_rpeepp = PL_rpeepp;
    PL_rpeepp = my_rpeep;

可插入式 runops

編譯樹在 runops 函式中執行。在 run.cdump.c 中有兩個 runops 函式。Perl_runops_debug 與 DEBUGGING 搭配使用,Perl_runops_standard 則在其他情況下使用。若要精細控制編譯樹的執行,可以提供您自己的 runops 函式。

最好複製現有的 runops 函式之一,並根據您的需求進行變更。然後,在 XS 檔案的 BOOT 區段中,新增下列程式碼行

PL_runops = my_runops;

此函式應盡可能有效率,以確保您的程式執行速度盡可能快。

編譯時間範圍掛鉤

從 perl 5.14 開始,可以使用 Perl_blockhook_register 連結到編譯時期的詞彙範圍機制。使用方式如下

STATIC void my_start_hook(pTHX_ int full);
STATIC BHK my_hooks;

BOOT:
    BhkENTRY_set(&my_hooks, bhk_start, my_start_hook);
    Perl_blockhook_register(aTHX_ &my_hooks);

這會安排在編譯每個詞彙範圍的開頭呼叫 my_start_hook。可用的連結如下

void bhk_start(pTHX_ int full)

這會在開始新的詞彙範圍後立即呼叫。請注意,類似下列的 Perl 程式碼

if ($x) { ... }

會建立兩個範圍:第一個從 ( 開始,full == 1,第二個從 { 開始,full == 0。兩個都以 } 結束,因此呼叫 startpre/post_end 會相符。此連結推送到儲存堆疊的任何內容都會在範圍結束前彈出(實際上是在 pre_post_end 連結之間)。

void bhk_pre_end(pTHX_ OP **o)

這會在詞彙範圍結束時呼叫,就在展開堆疊之前。o 是代表範圍的 optree 根目錄;它是一個雙重指標,因此您可以在需要時取代 OP。

void bhk_post_end(pTHX_ OP **o)

這會在詞彙範圍結束時呼叫,就在展開堆疊之後。o 如上所述。請注意,如果儲存堆疊中有呼叫字串 eval 的內容,pre_post_end 的呼叫可能會巢狀。

void bhk_eval(pTHX_ OP *const o)

在開始編譯 eval STRINGdo FILErequireuse 之前,在 eval 設定完後會呼叫此函式。o 是要求 eval 的 OP,通常會是 OP_ENTEREVALOP_DOFILEOP_REQUIRE

取得掛鉤函式後,需要一個 BHK 結構來放置這些函式。最好以靜態方式配置,因為一旦註冊後就無法釋放。函式指標應使用 BhkENTRY_set 巨集插入此結構,此巨集也會設定標記,指出哪些項目有效。如果出於某種原因需要動態配置 BHK,請務必在開始前將其歸零。

註冊後,沒有機制可以關閉這些掛鉤,因此如果需要這麼做,您需要自行執行。%^H 中的項目可能是最好的方式,因此效果是詞法範圍;但也可以使用 BhkDISABLEBhkENABLE 巨集來暫時開啟和關閉項目。您還應該知道,一般來說,在載入擴充套件之前至少會開啟一個範圍,因此您會看到一些沒有匹配 startpre/post_end 對。

使用 dump 函式檢查內部資料結構

為了協助除錯,原始檔 dump.c 包含許多函式,可產生內部資料結構的格式化輸出。

這些函式中使用最頻繁的是 Perl_sv_dump;它用於傾印 SV、AV、HV 和 CV。Devel::Peek 模組呼叫 sv_dump 以從 Perl 空間產生除錯輸出,因此該模組的使用者應該已經熟悉其格式。

Perl_op_dump 可用於傾印 OP 結構或其任何衍生結構,並產生類似於 perl -Dx 的輸出;事實上,Perl_dump_eval 會傾印正在評估的程式碼的主要根目錄,就像 -Dx 一樣。

其他有用的函數包括 Perl_dump_sub,它會將 GV 轉換為 op 樹狀結構,Perl_dump_packsubs 會對套件中所有子常式呼叫 Perl_dump_sub,如下所示:(幸好,這些都是 xsubs,因此沒有 op 樹狀結構)

(gdb) print Perl_dump_packsubs(PL_defstash)

SUB attributes::bootstrap = (xsub 0x811fedc 0)

SUB UNIVERSAL::can = (xsub 0x811f50c 0)

SUB UNIVERSAL::isa = (xsub 0x811f304 0)

SUB UNIVERSAL::VERSION = (xsub 0x811f7ac 0)

SUB DynaLoader::boot_DynaLoader = (xsub 0x805b188 0)

以及 Perl_dump_all,它會傾印儲存區中的所有子常式和主要根目錄的 op 樹狀結構。

如何支援多個解釋器和並行性

背景和 MULTIPLICITY

Perl 解釋器可以視為一個封閉的盒子:它有一個 API 可用於提供程式碼或讓它執行其他操作,但它也有供自己使用的函數。這很像一個物件,而且有一個方法可讓您建置 Perl,以便您可以擁有多個解釋器,其中一個解釋器表示為 C 結構,或位於特定於執行緒的結構中。這些結構包含所有內容,也就是該解釋器的狀態。

控制主要 Perl 建置風格的巨集是 MULTIPLICITY。MULTIPLICITY 建置有一個 C 結構,可封裝所有解釋器狀態,並作為「隱藏」的第一個引數傳遞給各種 perl 函數。MULTIPLICITY 使得多執行緒 perl 成為可能 (使用 ithreads 執行緒模型,與巨集 USE_ITHREADS 相關)。

PERL_IMPLICIT_CONTEXT 是 MULTIPLICITY 的舊式同義詞。

若要查看您是否有非常數資料,可以使用相容於 BSD (或 GNU) 的 nm

nm libperl.a | grep -v ' [TURtr] '

如果這顯示任何 Dd 符號(或可能是 Cc),表示您有非常數資料。grep 移掉的符號如下:Tt文字或程式碼,Rr唯讀(常數)資料,而 U 是 <未定義>,所引用的外部符號。

測試 t/porting/libperl.tlibperl.a 執行這類型的符號健全性檢查。

顯然,這一切都需要一種方法,讓 Perl 內部函式可以是將某種結構作為第一個引數的子常式,或將無任何內容作為第一個引數的子常式。為了讓這兩種截然不同的方式建置直譯器,Perl 原始碼(就像在許多其他情況中一樣)大量使用巨集和子常式命名慣例。

第一個問題:決定哪些函式會是公開 API 函式,哪些會是私人函式。所有名稱開頭為 S_ 的函式都是私人函式(將「S」想成「秘密」或「靜態」)。所有其他函式都以「Perl_」開頭,但函式名稱開頭為「Perl_」並不表示它是 API 的一部分。(請參閱 「內部函式」。)確定函式是 API 一部分最簡單的方法,是在 perlapi 中找到它的項目。如果它存在於 perlapi 中,表示它是 API 的一部分。如果不存在,而您認為它應該存在(即您在延伸模組中需要它),請在 https://github.com/Perl/perl5/issues 提交問題,說明您認為它應該存在的理由。

第二個問題:必須有一個語法,讓相同的子常式宣告和呼叫可以傳遞結構作為它們的第一個引數,或不傳遞任何內容。為了解決這個問題,子常式會以特定方式命名和宣告。以下是 Perl 內部使用的靜態函式常見開頭

STATIC void
S_incline(pTHX_ char *s)

STATIC 在 C 中會變成「static」,未來在某些組態中可能會 #define 為無任何內容。

公開函式(即內部 API 的一部分,但不一定允許在延伸模組中使用)會像這樣開頭

void
Perl_sv_setiv(pTHX_ SV* dsv, IV num)

pTHX_ 是許多巨集(在 perl.h 中)之一,用來隱藏直譯器內容的詳細資料。THX 代表「執行緒」、「這個」或「東西」,視情況而定。(而且,喬治·盧卡斯沒有參與其中。 :-) 第一個字元可能是 prototype(原型)的「p」、argument(引數)的「a」或 declaration(宣告)的「d」,所以我們有 pTHXaTHXdTHX,以及它們的變體。

當 Perl 在沒有設定 MULTIPLICITY 的選項下建置時,將沒有包含直譯器內容的第一個參數。pTHX_ 巨集中的尾隨底線表示巨集擴充在內容參數後需要一個逗號,因為後續還有其他參數。如果未定義 MULTIPLICITY,pTHX_ 將會被忽略,且子常式不會建立原型來接收額外的參數。當沒有其他明確參數時,會使用沒有尾隨底線的巨集形式。

當核心函式呼叫其他函式時,它必須傳遞內容。這通常會透過巨集隱藏起來。考慮 sv_setiv。它會擴充為類似以下的內容

#ifdef MULTIPLICITY
  #define sv_setiv(a,b)      Perl_sv_setiv(aTHX_ a, b)
  /* can't do this for vararg functions, see below */
#else
  #define sv_setiv           Perl_sv_setiv
#endif

這運作良好,表示 XS 作者可以開心地撰寫

sv_setiv(foo, bar);

並在 Perl 可編譯的所有模式下仍然讓它運作。

不過,這對於變數參數函式來說並無法如此順利運作,因為巨集暗示參數數量是事先已知的。因此,我們需要將它們完整拼寫出來,傳遞 aTHX_ 作為第一個參數(Perl 核心傾向於使用類似 Perl_warner 的函式來執行此操作),或使用不含內容的版本。

Perl_warner 的不含內容版本稱為 Perl_warner_nocontext,且不接收額外的參數。它會執行 dTHX; 以從執行緒本機儲存取得內容。我們會 #define warner Perl_warner_nocontext,以便擴充程式在犧牲效能的情況下取得原始碼相容性。(傳遞參數比從執行緒本機儲存取得參數便宜。)

瀏覽 Perl 標頭/來源時,您可以忽略 [pad]THXx。這些嚴格來說僅供核心內部使用。擴充程式和嵌入程式只需要知道 [pad]THX 即可。

那麼 dTHR 發生了什麼事?

dTHR 在 perl 5.005 中引入,以支援較舊的執行緒模型。較舊的執行緒模型現在使用 THX 機制來傳遞內容指標,因此 dTHR 不再有用。Perl 5.6.0 和更新版本仍保留它以維持向後原始碼相容性,但它被定義為空操作。

如何在擴充套件中使用所有這些內容?

當 Perl 使用 MULTIPLICITY 建置時,呼叫 Perl API 中任何函式的擴充套件將需要以某種方式傳遞初始內容參數。關鍵在於,您需要以一種方式撰寫它,即使在未啟用 MULTIPLICITY 的情況下,擴充套件仍可編譯。

有三個方法可以做到這一點。首先,簡單但低效率的方法,也是預設值,為了與擴充套件保持來源相容性:每當 #include XSUB.h 時,它會重新定義 aTHX 和 aTHX_ 巨集,以呼叫將傳回內容的函式。因此,類似於

sv_setiv(sv, num);

在 MULTIPLICITY 生效時,您的擴充套件將轉換為

Perl_sv_setiv(Perl_get_context(), sv, num);

否則轉換為

Perl_sv_setiv(sv, num);

您不必在擴充套件中執行任何新操作即可取得此結果;由於 Perl 函式庫提供 Perl_get_context(),因此所有操作都將正常運作。

第二種更有效率的方法是為您的 Foo.xs 使用下列範本

#define PERL_NO_GET_CONTEXT     /* we want efficiency */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

STATIC void my_private_function(int arg1, int arg2);

STATIC void
my_private_function(int arg1, int arg2)
{
    dTHX;       /* fetch context */
    ... call many Perl API functions ...
}

[... etc ...]

MODULE = Foo            PACKAGE = Foo

/* typical XSUB */

void
my_xsub(arg)
        int arg
    CODE:
        my_private_function(arg, 10);

請注意,與撰寫擴充套件的正常方式相比,唯一的兩個變更是在 include Perl 標頭之前新增 #define PERL_NO_GET_CONTEXT,然後在每個將呼叫 Perl API 的函式開頭新增 dTHX; 宣告。(您會知道哪些函式需要這個,因為 C 編譯器會抱怨這些函式中有未宣告的識別碼。)XSUB 本身不需要任何變更,因為 XS() 巨集已正確定義為在需要時傳入隱含內容。

第三種更有效率的方法是模仿 Perl 內部執行的方式

#define PERL_NO_GET_CONTEXT     /* we want efficiency */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

/* pTHX_ only needed for functions that call Perl API */
STATIC void my_private_function(pTHX_ int arg1, int arg2);

STATIC void
my_private_function(pTHX_ int arg1, int arg2)
{
    /* dTHX; not needed here, because THX is an argument */
    ... call Perl API functions ...
}

[... etc ...]

MODULE = Foo            PACKAGE = Foo

/* typical XSUB */

void
my_xsub(arg)
        int arg
    CODE:
        my_private_function(aTHX_ arg, 10);

此實作永遠不需要使用函式呼叫擷取內容,因為它總是作為額外參數傳遞。根據您對簡單性或效率的需求,您可以自由地混合前兩種方法。

請勿自行在 pTHX 後面加上逗號,請務必使用帶有底線巨集的形式,針對需要明確引數的函式,或是不帶引數的函式使用不帶引數的形式。

如果我從多個執行緒呼叫 perl,我需要做任何特別的事情嗎?

如果您在一個執行緒中建立直譯器,然後在另一個執行緒中呼叫它們,您需要確定 perl 自身的執行緒局部儲存 (TLS) 插槽在每個執行緒中都正確初始化。

perl_allocperl_clone API 函式會自動將 TLS 插槽設定為它們建立的直譯器,因此如果始終在建立直譯器的相同執行緒中存取直譯器,而且該執行緒之後沒有建立或呼叫任何其他直譯器,則不需要做任何特別的事情。如果不是這種情況,您必須在該特定直譯器上呼叫 Perl API 中的任何函式之前設定執行緒的 TLS 插槽。這可透過在該執行緒中呼叫 PERL_SET_CONTEXT 巨集作為您執行的第一件事來完成

/* do this before doing anything else with some_perl */
PERL_SET_CONTEXT(some_perl);

... other Perl API calls on some_perl go here ...

(您始終可以透過 PERL_GET_CONTEXT 取得目前的內容。)

未來的計畫和 PERL_IMPLICIT_SYS

就像 MULTIPLICITY 提供一種方式來彙整直譯器所知道的所有內容並傳遞這些內容一樣,也有計畫允許直譯器彙整它所知道的所有關於其執行環境的內容。這透過 PERL_IMPLICIT_SYS 巨集來啟用。目前它僅在 Windows 上與 USE_ITHREADS 搭配使用。

這允許提供一個額外指標(稱為「主機」環境)給所有系統呼叫。這使得所有系統事務都能維護自己的狀態,細分為七個 C 結構。這些是圍繞著預設 Perl 可執行檔的慣用系統呼叫(請參閱 win32/perllib.c)的薄封裝,但對於更雄心勃勃的主機(例如會執行 fork() 模擬的主機),所有額外的作業都必須假裝不同的直譯器實際上是不同的「程序」,會在此執行。

Perl 引擎/直譯器和主機是正交實體。一個程序中可以有一個或多個直譯器,以及一個或多個「主機」,它們之間可以自由關聯。

內部函數

所有會對外公開的 Perl 內部函數都加上 Perl_ 前綴,這樣它們就不會與 XS 函數或 Perl 嵌入式程序中使用的函數衝突。類似地,所有全域變數都以 PL_ 開頭。(依慣例,靜態函數以 S_ 開頭。)

在 Perl 核心內部(定義了 PERL_CORE),您可以使用或不使用 Perl_ 前綴來取得函數,這要歸功於 embed.h 中的一堆定義。請注意,擴充程式碼不應設定 PERL_CORE;這會公開完整的 Perl 內部結構,而且很可能會導致每個新 Perl 版本中的 XS 中斷。

檔案 embed.h 會自動從 embed.plembed.fnc 產生。embed.pl 也會建立內部函數的原型標頭檔、產生文件和許多其他零碎部分。當您新增一個新函數到核心或變更現有函數時,變更 embed.fnc 中表格中的資料也很重要。以下是該表格的範例條目

Apd |SV**   |av_fetch   |AV* ar|I32 key|I32 lval

第一欄是一組旗標,第二欄是傳回類型,第三欄是名稱。其後的欄位是引數。旗標記錄在 embed.fnc 的頂端。

如果您編輯 embed.plembed.fnc,您需要執行 make regen_headers 來強制重建 embed.h 和其他自動產生的檔案。

格式化列印 IV、UV 和 NV

如果您列印 IV、UV 或 NV,而不是 stdio(3) 樣式格式化代碼,例如 %d%ld%f,您應該使用以下巨集以確保可移植性

IVdf            IV in decimal
UVuf            UV in decimal
UVof            UV in octal
UVxf            UV in hexadecimal
NVef            NV %e-like
NVff            NV %f-like
NVgf            NV %g-like

這些巨集將處理 64 位元整數和 long double。例如

printf("IV is %" IVdf "\n", iv);

IVdf 將擴充為 IV 的正確格式。請注意,代碼如果使用 C++ 編譯,格式周圍需要有空格,以維持與其標準相容。

請注意,有不同的「long double」:Perl 將使用編譯器提供的任何內容。

如果您列印指標的位址,請使用 %p 或 UVxf 搭配 PTR2UV()。

格式化列印 SV

SV 的內容可以使用 SVf 格式列印,如下所示

Perl_croak(aTHX_ "This croaked because: %" SVf "\n", SVfARG(err_msg))

其中 err_msg 是 SV。

並非所有純量類型都可以列印。簡單的值當然可以:IV、UV、NV 或 PV 之一。此外,如果 SV 是某個值的參考,它將被解除參考並列印該值,或顯示該值的類型和其位址的資訊。列印任何其他類型 SV 的結果未定義,並且可能會導致直譯器崩潰。NV 使用類似 %g 的格式列印。

請注意,代碼如果使用 C++ 編譯,SVf 周圍需要有空格,以維持與其標準相容。

請注意,在 UTF-8 下列印到任何檔案處理常式都必須預期 UTF-8 才能獲得良好的結果並避免寬字元警告。對於典型的檔案處理常式,執行此操作的方法之一是使用 -C 參數呼叫 perl。(請參閱 perlrun 中的「-C [number/list]」

您可以使用此方法串接兩個純量

SV *var1 = get_sv("var1", GV_ADD);
SV *var2 = get_sv("var2", GV_ADD);
SV *var3 = newSVpvf("var1=%" SVf " and var2=%" SVf,
                    SVfARG(var1), SVfARG(var2));

SVf_QUOTEDPREFIX 類似於 SVf,但它限制了列印字元的數量,最多只顯示引數的第一個 PERL_QUOTEDPREFIX_LEN 個字元,並以雙引號呈現,且使用雙引號字串跳脫規則來跳脫內容。如果字串長於此長度,則會在尾隨的引號後加上省略號「...」。這適用於假設字串為類別名稱的錯誤訊息。

HvNAMEfHvNAMEf_QUOTEDPREFIX 類似於 SVf,但它們使用 HvNAME()HvNAMELEN()HvNAMEUTF8() 巨集從引數中擷取字串、長度和 utf8 旗標。這適用於直接從 stash HV 字串化類別名稱。

字串的格式化列印

如果您只想列印 7 位元 NUL 終止字串中的位元組,可以使用 %s(假設它們都真的只有 7 位元)。但如果值有可能編碼為 UTF-8 或包含高於 0x7F(因此為 8 位元)的位元組,則應該改用 UTF8f 格式。並將 UTF8fARG() 巨集用作其參數

chr * msg;

/* U+2018: \xE2\x80\x98 LEFT SINGLE QUOTATION MARK
   U+2019: \xE2\x80\x99 RIGHT SINGLE QUOTATION MARK */
if (can_utf8)
  msg = "\xE2\x80\x98Uses fancy quotes\xE2\x80\x99";
else
  msg = "'Uses simple quotes'";

Perl_croak(aTHX_ "The message is: %" UTF8f "\n",
                 UTF8fARG(can_utf8, strlen(msg), msg));

UTF8fARG 的第一個參數是布林值:如果字串為 UTF-8,則為 1;如果字串為原生位元組編碼(Latin1),則為 0。第二個參數是要列印的字串中的位元組數量。第三個也是最後一個參數是指向字串中第一個位元組的指標。

請注意,在 UTF-8 下列印到任何檔案處理常式都必須預期 UTF-8 才能獲得良好的結果並避免寬字元警告。對於典型的檔案處理常式,執行此操作的方法之一是使用 -C 參數呼叫 perl。(請參閱 perlrun 中的「-C [number/list]」

Size_tSSize_t 的格式化列印

執行此操作最通用的方法是將它們轉換為 UV 或 IV,並如 前一節 所述列印。

但如果您使用 PerlIO_printf(),使用 %z 長度修飾詞(表示 siZe)可以減少輸入和視覺混亂

PerlIO_printf("STRLEN is %zu\n", len);

此修飾詞不可攜帶,因此其使用應限制在 PerlIO_printf()

Ptrdiff_tintmax_tshort 和其他特殊大小的格式化列印

如果您使用 PerlIO_printf(),則有針對這些特殊情況的修飾詞。請參閱 perlfunc 中的「size」

指標轉整數與整數轉指標

由於指標大小不一定等於整數大小,請使用以下巨集來正確執行。

PTR2UV(pointer)
PTR2IV(pointer)
PTR2NV(pointer)
INT2PTR(pointertotype, integer)

例如

IV  iv = ...;
SV *sv = INT2PTR(SV*, iv);

AV *av = ...;
UV  uv = PTR2UV(av);

還有

PTR2nat(pointer)   /* pointer to integer of PTRSIZE */
PTR2ul(pointer)    /* pointer to unsigned long */

以及 PTRV,它會提供與指標大小相同的整數的原生型別,例如 unsignedunsigned long

例外處理

有幾個巨集可以在 XS 模組中執行非常基本的例外處理。您必須在包含 XSUB.h 之前定義 NO_XSLOCKS 才能使用這些巨集

#define NO_XSLOCKS
#include "XSUB.h"

如果您呼叫可能會 croak 的程式碼,可以使用這些巨集,但您需要在將控制權交回 Perl 之前執行一些清理工作。例如

dXCPT;    /* set up necessary variables */

XCPT_TRY_START {
  code_that_may_croak();
} XCPT_TRY_END

XCPT_CATCH
{
  /* do cleanup here */
  XCPT_RETHROW;
}

請注意,您必須始終重新擲出已捕獲的例外。使用這些巨集,無法僅捕獲例外並忽略它。如果您必須忽略例外,則必須使用 call_* 函數。

使用上述巨集的優點是您不必為 call_* 設定額外的函數,而且使用這些巨集比使用 call_* 更快。

原始碼文件

目前正在努力記錄內部函數並自動產生參考手冊——perlapi 就是這樣一本手冊,詳細說明了 XS 編寫人員可用的所有函數。 perlintern 是非 API 部分函數的自動產生手冊,理論上僅供內部使用。

原始碼文件是透過將 POD 註解放入 C 原始碼中建立的,如下所示

/*
=for apidoc sv_setiv

Copies an integer into the given SV.  Does not handle 'set' magic.  See
L<perlapi/sv_setiv_mg>.

=cut
*/

如果您將函數新增至 Perl 核心,請嘗試提供一些文件。

向下相容性

Perl API 會隨著時間而改變。新的函數會被新增,或現有函數的介面會被變更。 Devel::PPPort 模組會嘗試為其中一些變更提供相容性程式碼,因此 XS 編寫人員在支援多個 Perl 版本時不必自己編寫程式碼。

Devel::PPPort 會產生一個 C 標頭檔 ppport.h,也可以當作 Perl 程式碼執行。若要產生 ppport.h,請執行

perl -MDevel::PPPort -eDevel::PPPort::WriteFile

除了檢查現有的 XS 程式碼,此程式碼也可以用來使用 --api-info 命令列開關擷取各種 API 呼叫的相容性資訊。例如

% perl ppport.h --api-info=sv_magicext

有關詳細資訊,請參閱 perldoc ppport.h

Unicode 支援

Perl 5.6.0 引進了 Unicode 支援。對於移植者和 XS 編寫者來說,瞭解此支援並確保他們編寫的程式碼不會損壞 Unicode 資料非常重要。

Unicode 到底是什麼?

在久遠、比較不開化的時代,我們都使用 ASCII。大多數人都是這樣,反正。ASCII 的最大問題在於它是美國的。嗯,這其實不是問題;問題在於它對不使用羅馬字母的人來說不太有用。以前會發生的是,特定語言會將自己的字母表塞進序列的上層範圍,介於 128 到 255 之間。當然,我們後來得到了許多不太算是 ASCII 的變體,而它作為標準的整個重點就消失了。

更糟的是,如果你有像中文或日文這樣的語言,有數百或數千個字元,那麼你真的無法將它們塞進區區 256 個字元中,所以他們必須完全忘記 ASCII,並使用數字對來建立自己的系統來表示一個字元。

為了修正這個問題,一些人成立了 Unicode, Inc.,並製作了一個新的字元集,包含所有你能想到的字元,甚至更多。有許多方式可以表示這些字元,而 Perl 使用的方式稱為 UTF-8。UTF-8 使用可變數量的位元組來表示一個字元。你可以在 perlunicode 中瞭解更多有關 Unicode 和 Perl 的 Unicode 模型。

(在 EBCDIC 平台上,Perl 改用 UTF-EBCDIC,這是專門為 EBCDIC 平台調整過的 UTF-8 形式。以下我們只討論 UTF-8。UTF-EBCDIC 類似於 UTF-8,但細節不同。巨集會隱藏這些差異,只要記住以下顯示的特定數字和位元模式在 UTF-EBCDIC 中會有所不同即可。)

如何辨識 UTF-8 字串?

您無法辨識。這是因為 UTF-8 資料儲存在位元組中,就像非 UTF-8 資料一樣。Unicode 字元 200(對您這些十六進位類型來說是 0xC8),帶有重音符號的大寫 E,以兩個位元組 v196.172 表示。不幸的是,非 Unicode 字串 chr(196).chr(172) 也有相同的位元組順序。因此,您無法透過查看來辨識,這使得 Unicode 輸入成為一個有趣的問題。

一般來說,您必須知道自己在處理什麼,否則您必須猜測。API 函式 is_utf8_string 可以提供協助;它會告訴您字串是否只包含有效的 UTF-8 字元,而且隨著字串長度增加,非 UTF-8 字串看起來像有效 UTF-8 的機率會非常快速地變得很小。在逐字元基礎上,isUTF8_CHAR 會告訴您字串中的目前字元是否為有效的 UTF-8。

UTF-8 如何表示 Unicode 字元?

如上所述,UTF-8 使用可變數量的位元組來儲存一個字元。值為 0...127 的字元儲存在一個位元組中,就像傳統的 ASCII 一樣。字元 128 儲存在 v194.128 中;這一直持續到字元 191,也就是 v194.191。現在我們用完了位元組(191 的二進位是 10111111),所以我們繼續;字元 192 是 v195.128。以此類推,在字元 2048 時移到三個位元組。"Unicode 編碼" 中的 perlunicode 有關於其運作方式的圖片。

假設你知道自己正在處理 UTF-8 字串,你可以使用 UTF8SKIP 巨集找出其中的第一個字元長度

char *utf = "\305\233\340\240\201";
I32 len;

len = UTF8SKIP(utf); /* len is 2 here */
utf += len;
len = UTF8SKIP(utf); /* len is 3 here */

跳過 UTF-8 字串中字元的另一種方式是使用 utf8_hop,它會取得一個字串和要跳過的字元數。不過,你必須自行檢查邊界,因此不要輕易使用它。

多位元組 UTF-8 字元中的所有位元組都會設定高位元,因此你可以測試是否需要對這個字元執行特殊處理,如下所示(UTF8_IS_INVARIANT() 是巨集,用於測試位元組是否即使在 UTF-8 中也編碼為單一位元組)

U8 *utf;     /* Initialize this to point to the beginning of the
                sequence to convert */
U8 *utf_end; /* Initialize this to 1 beyond the end of the sequence
                pointed to by 'utf' */
UV uv;	 /* Returned code point; note: a UV, not a U8, not a
                char */
STRLEN len; /* Returned length of character in bytes */

if (!UTF8_IS_INVARIANT(*utf))
    /* Must treat this as UTF-8 */
    uv = utf8_to_uvchr_buf(utf, utf_end, &len);
else
    /* OK to treat this character as a byte */
    uv = *utf;

你也可以在那個範例中看到我們使用 utf8_to_uvchr_buf 取得字元的數值;反向函式 uvchr_to_utf8 可用於將 UV 放入 UTF-8

if (!UVCHR_IS_INVARIANT(uv))
    /* Must treat this as UTF8 */
    utf8 = uvchr_to_utf8(utf8, uv);
else
    /* OK to treat this character as a byte */
    *utf8++ = uv;

如果你曾經遇到必須比對 UTF-8 和非 UTF-8 字元的情況,你必須使用上述函式將字元轉換為 UV。在此情況下,你不能跳過 UTF-8 字元。如果你這樣做,你將失去比對非 UTF-8 高位元字元的能力;例如,如果你的 UTF-8 字串包含 v196.172,而你跳過那個字元,你將永遠無法在非 UTF-8 字串中比對 chr(200)。所以不要這樣做!

(請注意,我們不必在上述範例中測試不變字元。這些函式會處理任何格式良好的 UTF-8 輸入。只是在不需要時避免函式開銷會比較快。)

Perl 如何儲存 UTF-8 字串?

目前,Perl 處理 UTF-8 字串和非 UTF-8 字串的方式略有不同。SV 中的旗標 SVf_UTF8 表示字串在內部編碼為 UTF-8。沒有它,位元組值就是碼位編號,反之亦然。此旗標只有在 SV 是 SvPOK 或透過 SvPV 或類似巨集字串化後才有意義。你可以使用以下巨集檢查和操作此旗標

SvUTF8(sv)
SvUTF8_on(sv)
SvUTF8_off(sv)

此旗標對 Perl 處理字串的方式有重要的影響:如果 UTF-8 資料沒有適當地區分,正規表示式、lengthsubstr 和其他字串處理作業將會有不良(錯誤)的結果。

問題發生在您有字串,例如,並未標記為 UTF-8,且包含可能為 UTF-8 的位元組序列,特別是在組合非 UTF-8 和 UTF-8 字串時。

永遠不要忘記,SVf_UTF8 旗標與 PV 值是分開的;您需要確定在操作 SV 時不會意外將其關閉。更具體地說,您不能這樣做

SV *sv;
SV *nsv;
STRLEN len;
char *p;

p = SvPV(sv, len);
frobnicate(p);
nsv = newSVpvn(p, len);

char* 字串並未告訴您完整的故事,您無法僅透過複製字串值來複製或重建 SV。檢查舊的 SV 是否設定了 UTF8 旗標(在 SvPV 呼叫之後),並採取相應的動作

p = SvPV(sv, len);
is_utf8 = SvUTF8(sv);
frobnicate(p, is_utf8);
nsv = newSVpvn(p, len);
if (is_utf8)
    SvUTF8_on(nsv);

在上面,您的 frobnicate 函式已變更為知道是否處理 UTF-8 資料,以便它可以適當地處理字串。

由於僅將 SV 傳遞給 XS 函式並複製 SV 的資料不足以複製 UTF8 旗標,因此將 char * 傳遞給 XS 函式更不正確。

為了完全通用,請使用 DO_UTF8 巨集來查看 SV 中的字串是否視為 UTF-8。這會考慮是否從 use bytes 的範圍內進行呼叫 XS 函式。如果是這樣,組成 UTF-8 字串的基礎位元組將會公開,而不是它們所代表的字元。但此指令僅應真正用於除錯,以及可能在位元組層級進行低階測試。因此,大多數 XS 程式碼不需要關注這一點,但 perl 核心中的各種區域確實需要支援它。

而且這還不是全部。從 Perl v5.12 開始,未以 UTF-8 編碼的字串在各種情況下也可能被視為 Unicode(請參閱 perlunicode 中的「ASCII 規則與 Unicode 規則」)。這只會對序數介於 128 到 255 之間的字元造成問題,而且它們在 ASCII 與 Unicode 規則下的行為會以您的程式碼在意的方式有所不同(請參閱 perlunicode 中的「「Unicode 錯誤」」)。由於這可能會變更,因此沒有已發布的 API 可供處理,但您可以查看 pp.cpp_lc 的程式碼,了解目前如何執行。

如何將 Perl 字串傳遞給 C 函式庫?

從概念上來說,Perl 字串是不透明的碼點序列。許多 C 函式庫預期其輸入為「傳統」C 字串,這是由 1-255 的八位元組陣列組成,並以 NUL 位元組結尾。在撰寫 Perl 和 C 函式庫之間的介面時,您的工作是定義 Perl 和該函式庫之間的對應。

一般來說,SvPVbyte 和相關巨集很適合這項任務。這些假設您的 Perl 字串是「位元組字串」,亦即,是 Perl 中的原始、未解碼輸入,或已預先編碼為例如 UTF-8。

或者,如果您的 C 函式庫預期 UTF-8 文字,您可以使用 SvPVutf8 和相關巨集。這與編碼為 UTF-8 然後呼叫對應的 SvPVbyte 相關巨集具有相同的效果。

有些 C 函式庫可能需要其他編碼(例如 UTF-16LE)。若要將 Perl 字串傳遞給此類函式庫,您必須在 Perl 中執行該編碼,然後使用 SvPVbyte,或使用中間 C 函式庫將 Perl 儲存字串的方式轉換為所需的編碼。

還要小心,Perl 字串中的 NUL 不要讓 C 函式庫混淆。如果可能,請將字串長度傳遞給 C 函式庫;如果不可能,請考慮拒絕包含 NUL 位元的字串。

關於 SvPVSvPV_nolen

考慮一個 3 個字元的 Perl 字串 $foo = "\x64\x78\x8c"。Perl 可以用兩種方式儲存這 3 個字元

現在,假設您將 $foo 轉換為 C 字串,如下所示

STRLEN strlen;
char *str = SvPV(foo_sv, strlen);

此時,str 可以指向 3 位元組的 C 字串或 4 位元組的 C 字串。

一般來說,我們希望 str 相同,無論 Perl 如何儲存 $foo,因此這裡的歧義是不希望的。SvPVbyteSvPVutf8 透過提供可預測的輸出解決了這個問題:如果您的 C 函式庫需要位元組字串,請使用 SvPVbyte;如果需要 UTF-8,請使用 SvPVutf8

如果您的 C 函式庫碰巧支援這兩種編碼,則 SvPV(始終與對 SvUTF8 的查詢搭配使用!)可能是安全的,而且(稍微)更有效率。

測試提示:在您的測試中使用 utf8upgradedowngrade 函式,以確保無論 Perl 的內部編碼如何,都能一致處理。

如何將字串轉換為 UTF-8?

如果您混合了 UTF-8 和非 UTF-8 字串,則有必要將非 UTF-8 字串升級為 UTF-8。如果您有 SV,最簡單的方法是

sv_utf8_upgrade(sv);

但是,您不能這樣做,例如

if (!SvUTF8(left))
    sv_utf8_upgrade(left);

如果您在二元運算子中這樣做,您實際上會更改進入運算子的其中一個字串,而且儘管最終使用者不應該注意到,但它可能會在有缺陷的程式碼中造成問題。

相反地,bytes_to_utf8 會提供其字串引數的 UTF-8 編碼副本。這對於讓資料可用於比較等用途很有用,而不會損害原始 SV。還有 utf8_to_bytes 可用於反向轉換,但顯然地,如果字串包含任何無法用單一位元組表示、大於 255 的字元,就會失敗。

如何比較字串?

"sv_cmp" in perlapi"sv_cmp_flags" in perlapi 執行兩個 SV 的字彙比較,並適當地處理 UTF-8。不過,請注意,Unicode 透過 Unicode::Collate 模組指定了更精緻的校對機制。

若要比較兩個字串是否相等/不相等,你可以像往常一樣使用 memEQ()memNE(),但字串必須同時編碼為 UTF-8 或非 UTF-8。

若要比較兩個字串不分大小寫,請使用 foldEQ_utf8()(字串不需要具有相同的 UTF-8)。

還有其他需要知道的嗎?

沒有什麼。只要記住這些事情

自訂運算子

自訂運算子支援是一種實驗性功能,可讓你定義自己的運算子。這主要是為了讓 Perl 核心中的其他語言的直譯器能夠建置,但它也允許透過建立「巨集運算子」(執行通常一起執行之多個運算子功能的運算子,例如 gvsv, gvsv, add),進行最佳化。

此功能實作為新的運算子類型 OP_CUSTOM。Perl 核心對此運算子類型「一無所知」,因此不會參與任何最佳化。這也表示你可以定義自訂運算子為任何運算子結構 - 一元、二元、清單等 - 你喜歡。

了解自訂運算子無法為你做什麼很重要。它們不會讓你直接新增新的 Perl 語法。它們甚至不會讓你直接新增新的關鍵字。事實上,它們根本不會改變 Perl 編譯程式的任何方式。你必須在 Perl 編譯程式後,自己進行這些變更。你可以使用 CHECK 區塊和 B::Generate 模組來操作 op 樹狀結構,或使用 optimize 模組來新增自訂 peephole 最佳化程式來執行此操作。

執行此操作時,你可以透過建立類型為 OP_CUSTOMop_ppaddr 為你自己的 PP 函式的 op,來取代一般的 Perl op。這應該在 XS 程式碼中定義,並且應該看起來像 pp_*.c 中的 PP op。你負責確保你的 op 從堆疊中取得適當數量的值,而且你負責在必要時新增堆疊標記。

你還應該向 Perl 詮譯器「註冊」你的 op,以便它可以產生合理的錯誤和警告訊息。由於在一個「邏輯」op 類型 OP_CUSTOM 中可以有多個自訂 op,因此 Perl 會使用 o->op_ppaddr 的值來判斷它正在處理哪個自訂 op。你應該為你使用的每個 ppaddr 建立一個 XOP 結構,使用 XopENTRY_set 設定自訂 op 的屬性,並使用 Perl_custom_op_register 針對 ppaddr 註冊結構。一個簡單的範例可能如下所示

static XOP my_xop;
static OP *my_pp(pTHX);

BOOT:
    XopENTRY_set(&my_xop, xop_name, "myxop");
    XopENTRY_set(&my_xop, xop_desc, "Useless custom op");
    Perl_custom_op_register(aTHX_ my_pp, &my_xop);

結構中可用的欄位為

xop_name

你的 op 的簡短名稱。這將包含在一些錯誤訊息中,並且也會由 B 模組作為 $op->name 回傳,因此它會出現在 B::Concise 等模組的輸出中。

xop_desc

op 功能的簡短說明。

xop_class

這個 op 使用的各種 *OP 結構。這應該是來自 op.hOA_* 常數之一,即

OA_BASEOP
OA_UNOP
OA_BINOP
OA_LOGOP
OA_LISTOP
OA_PMOP
OA_SVOP
OA_PADOP
OA_PVOP_OR_SVOP

這應解釋為「PVOP」而已。_OR_SVOP 是因為唯一的核心 PVOPOP_TRANS,有時可以是 SVOP

OA_LOOP
OA_COP

不應使用其他 OA_* 常數。

xop_peep

這個成員的類型為 Perl_cpeep_t,會擴充為 void (*Perl_cpeep_t)(aTHX_ OP *o, OP *oldop)。如果設定此項目,當 peephole 最佳化器遇到此類型的運算子時,此函式會從 Perl_rpeep 呼叫。o 是需要最佳化的運算子;oldop 是先前最佳化的運算子,其 op_next 指向 o

B::Generate 直接支援依名稱建立自訂運算子。

堆疊

上述說明偶爾會提到「堆疊」,但事實上 perl 解譯器中有許多類似堆疊的資料結構。在沒有其他限定的情況下,「堆疊」通常是指值堆疊。

各種堆疊有不同的用途,並以略微不同的方式運作。以下會說明它們的差異。

值堆疊

此堆疊儲存一般 perl 程式碼運作的值,通常是陳述式中表達式的中間值。堆疊本身是由 SV 指標陣列組成。

此堆疊的基底由類型為 SV ** 的解譯器變數 PL_stack_base 指向。

堆疊的頂端為 PL_stack_sp,指向最近推入的項目。

使用 PUSHs() 巨集或其上述變體將項目推入堆疊中;XPUSHs()mPUSHs()mXPUSHs() 和類型化版本。請仔細注意,這些巨集的非 X 版本不會檢查堆疊大小,並假設它足夠大。這些必須與堆疊大小的適當檢查配對,例如 EXTEND 巨集,以確保它足夠大。例如

EXTEND(SP, 4);
mPUSHi(10);
mPUSHi(20);
mPUSHi(30);
mPUSHi(40);

這比在四個獨立的 mXPUSHi() 呼叫中執行四次獨立檢查的效能稍高。

作為進一步的效能最佳化,各種 PUSH 巨集都使用局部變數 SP 進行操作,而不是使用直譯器全域變數 PL_stack_sp。此變數由 dSP 巨集宣告 - 儘管 XSUB 和類似巨集通常會暗示它,因此您很少需要直接考慮它。宣告後,PUSH 巨集將僅對此局部變數進行操作,因此在呼叫任何其他 perl 核心函數之前,您必須使用 PUTBACK 巨集將值從局部 SP 變數傳回直譯器變數。類似地,在呼叫 perl 核心函數(它可能已移動堆疊或推入/彈出值)後,您必須使用 SPAGAIN 巨集,該巨集會從直譯器刷新局部 SP 值。

使用 POPs 巨集或其類型化版本從堆疊中彈出項目,還有一個巨集 TOPs,它會檢查最頂端的項目而不移除它。

特別注意,值堆疊上的 SV 指標不會增加所引用的 xV 的整體參考計數。如果您要將新建立的 xV 推入堆疊,您必須安排在適當的時間銷毀它們;通常是使用 mPUSH* 巨集之一或 sv_2mortal() 將 xV 設為 mortal。

標記堆疊

值堆疊將個別 perl 標量值儲存在表達式之間作為暫存器。有些 perl 表達式會對整個清單進行操作;為此,我們需要知道堆疊中每個清單的開始位置。這就是標記堆疊的目的。

標記堆疊將整數儲存為 I32 值,這是清單開始前值堆疊的高度;因此,標記本身實際上指向清單前一個值堆疊條目。清單本身從 mark + 1 開始。

此堆疊的基礎是由型別為 I32 * 的解釋器變數 PL_markstack 指向的。

堆疊的頭部是 PL_markstack_ptr,並指向最近推入的項目。

使用 PUSHMARK() 巨集將項目推入堆疊。即使堆疊本身將(值)堆疊索引儲存為整數,也應直接提供堆疊指標給 PUSHMARK 巨集;它會透過與 PL_stack_sp 變數比較來計算索引偏移量。因此,執行此操作的程式碼幾乎總是

PUSHMARK(SP);

使用 POPMARK 巨集從堆疊中彈出項目。還有一個巨集 TOPMARK,用於檢查最上層的項目而不將其移除。這些巨集直接傳回 I32 索引值。還有 dMARK 巨集,用於宣告一個名為 mark 的新的 SV 雙指標變數,指向標記的堆疊槽;這是 C 程式碼在對堆疊上提供的清單進行操作時會使用的常見巨集。

如上所述,mark 變數本身會指向清單開始前的值堆疊上最近推入的值,因此清單本身從 mark + 1 開始。清單的值可以用類似於以下的程式碼進行反覆運算

for(SV **svp = mark + 1; svp <= PL_stack_sp; svp++) {
  SV *item = *svp;
  ...
}

特別注意在清單已為空的情況下,mark 將等於 PL_stack_sp

由於 mark 變數會轉換為值堆疊上的指標,因此如果在函式中呼叫 EXTEND 或任何 XPUSH 巨集,則必須特別小心,因為堆疊可能需要移動才能延伸,因此現有的指標現在會無效。如果這可能會造成問題,可能的解決方案是將標記偏移量追蹤為整數,並在堆疊移動後稍後追蹤標記本身。

I32 markoff = POPMARK;

...

SP **mark = PL_stack_base + markoff;

暫存堆疊

如上所述,主值堆疊上的 xV 參照不會增加 xV 的參照計數,因此會使用另一種機制來追蹤堆疊上暫時值何時必須釋放。這就是暫時堆疊的工作。

暫時堆疊會儲存 xV 的指標,其參照計數將很快遞減。

此堆疊的基底由型別為 SV ** 的直譯器變數 PL_tmps_stack 指向。

堆疊的頭部由 PL_tmps_ix 編制索引,此整數儲存陣列中最近推入項目的索引。

沒有公開的 API 可直接將項目推入暫時堆疊。相反地,API 函式 sv_2mortal() 用於將 xV 設為暫時,將其位址新增到暫時堆疊中。

同樣地,沒有公開的 API 可從暫時堆疊中讀取值。相反地,會使用巨集 SAVETMPSFREETMPSSAVETMPS 巨集會建立暫時堆疊的基本層級,方法是將 PL_tmps_ix 的目前值擷取到 PL_tmps_floor 中,並將前一個值儲存到儲存堆疊中。此後,每當呼叫 FREETMPS 時,自該層級以來已推入的所有暫時值都會被回收。

儘管通常會在 ENTER/LEAVE 對中成對看到這兩個巨集,但不必將它們配對。允許自最近一次 SAVETMPS 以來多次呼叫 FREETMPS;例如,在迴圈中反覆運算清單的元素。雖然可以在範圍對中多次呼叫 SAVETMPS,但不太可能有用。後續呼叫會將暫時層級往上移動,因此實際上會限制現有的暫時值,僅在範圍結束時才會釋放。

儲存堆疊

儲存堆疊由 Perl 用來實作 local 關鍵字和其他類似行為;在離開目前範圍時需要執行的任何清理作業。推入此堆疊的項目通常會擷取某些內部變數或狀態的目前值,當範圍因離開、returndiegoto 或其他原因而展開時,這些值會被還原。

其他 Perl 內部堆疊會儲存所有相同型別的個別項目(通常是 SV 指標或整數),而推入儲存堆疊的項目由許多不同型別組成,具有多個欄位。例如,SAVEt_INT 型別需要同時儲存要還原的 int 變數的位址,以及要還原的值。此資訊可以使用 struct 的欄位來儲存,但必須夠大,才能在最大情況下儲存三個指標,這會在大部分較小的情況下浪費許多空間。

相反地,堆疊會將資訊儲存在ANY結構的變數長度編碼中。最後推入的值儲存在UV欄位中,該欄位編碼前一個項目所持有的項目類型;其數量和類型取決於要儲存的項目類型。類型欄位最後推入,因為在從堆疊中彈出項目時,這會是第一個彈出的欄位。

此堆疊的基底由PL_savestack解釋器變數指向,其類型為ANY *

堆疊的頭部由PL_savestack_ix索引,此整數儲存陣列中應推入下一個項目的索引。(請注意,這與大多數其他堆疊不同,後者會參照最近推入的項目)。

項目會透過使用各種SAVE...()巨集推入儲存堆疊。其中許多巨集會取得變數,並將其位址和目前值儲存在儲存堆疊上,確保在範圍結束時會還原該值。

SAVEI8(i8)
SAVEI16(i16)
SAVEI32(i32)
SAVEINT(i)
...

還有各種其他特殊用途的巨集,可儲存特定類型或感興趣的值。SAVETMPS已在上面提到。其他巨集包括SAVEFREEPV,其會安排釋放PV(即字串緩衝區),或SAVEDESTRUCTOR,其會安排在範圍結束時呼叫指定的函式指標。可以在scope.h中找到此類巨集的完整清單。

沒有公開的API可從儲存堆疊彈出個別值或項目。相反地,透過範圍堆疊,ENTERLEAVE配對形成一種開始和停止巢狀範圍的方法。透過LEAVE離開巢狀範圍會還原自最近一次ENTER以來已推入的所有儲存值。

範圍堆疊

與對應於值堆疊的標記堆疊一樣,範圍堆疊與儲存堆疊形成一對。範圍堆疊會儲存巢狀範圍開始時的儲存堆疊高度,並允許在離開範圍時將儲存堆疊解回到該點。

當 perl 在啟用除錯的情況下建置時,這個堆疊的第二個部分會儲存描述堆疊內容類型的人類可讀字串名稱。每個 push 操作都會儲存名稱以及儲存堆疊的高度,每個 pop 操作都會檢查最上層的名稱與預期的一致性,如果名稱不符就會導致斷言失敗。

這個堆疊的基底由型別為 I32 * 的詮釋器變數 PL_scopestack 指向。如果啟用,範圍堆疊名稱會儲存在由型別為 const char **PL_scopestack_name 指向的獨立陣列中。

堆疊的頭部由 PL_scopestack_ix 編制索引,這個整數會儲存下一個項目應該被 push 進去的陣列或陣列的索引。(請注意,這與大多數其他堆疊不同,後者會參照最近 push 進去的項目)。

值會使用 ENTER 巨集 push 到範圍堆疊,這個巨集會開始一個新的巢狀範圍。然後,任何 push 到儲存堆疊的項目都會在 LEAVE 巨集的下一個巢狀呼叫中還原。

動態範圍和內容堆疊

注意:此區段描述非公開的內部 API,可能會在沒有通知的情況下變更。

內容堆疊簡介

在 Perl 中,動態範圍是指子常式呼叫、eval 等的執行時間巢狀,以及區塊範圍的進入和離開。例如,local 化變數的還原是由動態範圍決定的。

Perl 追蹤動態範圍的資料結構稱為「context stack」,它是一個 PERL_CONTEXT 結構陣列,本身是一個所有 context 類型的巨型聯合。每當進入新的範圍(例如區塊、for 迴圈或子常式呼叫)時,就會將新的 context 項目推入堆疊。同樣地,在離開區塊或從子常式呼叫等中返回時,會彈出 context。由於 context stack 代表目前的動態範圍,因此可以搜尋它。例如,next LABEL 會向後搜尋堆疊,尋找與標籤相符的迴圈 context;return 會彈出 context,直到找到子常式或 eval context 或類似項目;caller 會檢查堆疊上的子常式 context。

每個 context 項目都標記有 context 類型 cx_type。典型的 context 類型包括 CXt_SUBCXt_EVAL 等,以及代表基本範圍(由 pp_enter 推入)和排序區塊的 CXt_BLOCKCXt_NULL。類型會決定 context 聯合的哪一部分有效。

context 結構中的主要區分在於替換範圍(CXt_SUBST)和區塊範圍,也就是其他所有範圍。前者僅在執行 s///e 時使用,在此不再進一步討論。

所有區塊範圍類型都共用一個共同的基礎,對應於 CXt_BLOCK。它會儲存各種與範圍相關的變數的舊值,例如 PL_curpm,以及有關目前範圍的資訊,例如 gimme。在範圍結束時,會還原舊變數。

特定區塊範圍類型會儲存額外的每種類型資訊。例如,CXt_SUB 會儲存目前執行的 CV,而各種 for 迴圈類型可能會保留原始迴圈變數 SV。在範圍結束時,會處理每種類型的資料;例如,CV 的參考計數會遞減,而原始迴圈變數會還原。

巨集 cxstack 會傳回目前內容堆疊的基礎,而 cxstack_ix 則為該堆疊中目前框架的索引。

事實上,內容堆疊實際上是堆疊系統的一部分;每當執行不尋常的動作,例如呼叫 DESTROY 或繫結處理常式時,就會推入一個新的堆疊,然後在最後彈出。

請注意,此處所述的 API 在 perl 5.24 中已大幅變更;在此之前,會使用 PUSHBLOCKPOPSUB 等大型巨集;在 5.24 中,它們已被以下所述的內嵌靜態函式取代。此外,這些巨集/函式的順序和詳細運作方式已在許多方面發生變更,通常是細微的變更。特別是,它們不會處理儲存儲存堆疊和暫存堆疊位置,並且與新函式相比,需要額外的 ENTERSAVETMPSLEAVE。舊式巨集將不再進一步說明。

推入內容

對於推入新內容,兩個基本函式為 cx = cx_pushblock(),它會推入一個新的基本內容區塊並傳回其位址,以及一系列具有類似名稱的函式,例如 cx_pushsub(cx),它們會填入 cx 結構中額外的類型相依欄位。請注意,CXt_NULLCXt_BLOCK 沒有自己的推入函式,因為它們不會儲存 cx_pushblock 所推入的資料以外的任何資料。

內容結構的欄位和 cx_* 函式的引數可能會在 perl 版本之間變更,代表對該版本來說方便或有效率的任何內容。

典型的內容堆疊推入可以在 pp_entersub 中找到;以下顯示非 XS 呼叫的簡化和精簡範例,以及顯示每個函式大致功能的註解。

dMARK;
U8 gimme      = GIMME_V;
bool hasargs  = cBOOL(PL_op->op_flags & OPf_STACKED);
OP *retop     = PL_op->op_next;
I32 old_ss_ix = PL_savestack_ix;
CV *cv        = ....;

/* ... make mortal copies of stack args which are PADTMPs here ... */

/* ... do any additional savestack pushes here ... */

/* Now push a new context entry of type 'CXt_SUB'; initially just
 * doing the actions common to all block types: */

cx = cx_pushblock(CXt_SUB, gimme, MARK, old_ss_ix);

    /* this does (approximately):
        CXINC;              /* cxstack_ix++ (grow if necessary) */
        cx = CX_CUR();      /* and get the address of new frame */
        cx->cx_type        = CXt_SUB;
        cx->blk_gimme      = gimme;
        cx->blk_oldsp      = MARK - PL_stack_base;
        cx->blk_oldsaveix  = old_ss_ix;
        cx->blk_oldcop     = PL_curcop;
        cx->blk_oldmarksp  = PL_markstack_ptr - PL_markstack;
        cx->blk_oldscopesp = PL_scopestack_ix;
        cx->blk_oldpm      = PL_curpm;
        cx->blk_old_tmpsfloor = PL_tmps_floor;

        PL_tmps_floor        = PL_tmps_ix;
    */


/* then update the new context frame with subroutine-specific info,
 * such as the CV about to be executed: */

cx_pushsub(cx, cv, retop, hasargs);

    /* this does (approximately):
        cx->blk_sub.cv          = cv;
        cx->blk_sub.olddepth    = CvDEPTH(cv);
        cx->blk_sub.prevcomppad = PL_comppad;
        cx->cx_type            |= (hasargs) ? CXp_HASARGS : 0;
        cx->blk_sub.retop       = retop;
        SvREFCNT_inc_simple_void_NN(cv);
    */

請注意,cx_pushblock() 設定兩個新樓層:用於引數堆疊(到 MARK)和暫存堆疊(到 PL_tmps_ix)。在這個範圍層級執行時,每個 nextstate(以及其他)會將引數和暫存堆疊層級重設為這些樓層。請注意,由於 cx_pushblock 使用 PL_tmps_ix 的目前值,而不是將其傳遞為引數,這決定了應該在何時呼叫 cx_pushblock。特別是,任何應該只在範圍結束時釋放(而不是在下一 nextstate)的新暫存都應該先建立。

大多數 cx_pushblock 的呼叫者會將新的引數堆疊樓層設定為前一個堆疊框架的頂端,但對於 CXt_LOOP_LIST,它會將在堆疊上反覆運算的項目儲存起來,因此會將 blk_oldsp 設定為這些項目的頂端。請注意,blk_oldsp 與其名稱相反,並不總是代表在範圍結束時要將 PL_stack_sp 還原到的值。

請注意,將 PL_savestack_ix 提早擷取到 old_ss_ix,稍後會將其作為引數傳遞給 cx_pushblock。在 pp_entersub 的情況下,這是因為雖然需要儲存的大部分值都儲存在內容結構的欄位中,但只有在偵錯器執行時才需要儲存額外的值,而且針對這種罕見情況擴充結構沒有意義。因此,它會儲存在儲存堆疊中。由於這個值會在推入內容之前計算並儲存,因此有必要將 PL_savestack_ix 的舊值傳遞給 cx_pushblock,以確保在範圍結束時會釋放儲存的值。對於 cx_pushblock 的大多數使用者,如果不需要在儲存堆疊上推入任何東西,PL_savestack_ix 會直接作為引數傳遞給 cx_pushblock

請注意,如果可以,應該將值儲存在內容結構中,而不是儲存在儲存堆疊上;這樣快很多。

通常 cx_pushblock 之後應該立即接著適當的 cx_pushfoo,中間沒有任何東西;這是因為如果中間的程式碼可能會死掉(例如,警告升級為致命),則 dounwind 中的內容堆疊展開程式碼會看到(在上述範例中)一個 CXt_SUB 內容框架,但沒有設定所有子常式特定欄位,而且很快就會發生崩潰。

必須將這兩個分開時,請先將類型設定為 CXt_NULLCXt_BLOCK,然後在執行 cx_pushfoo 時再將其變更為 CXt_foo。這正是 pp_enteriter 在確定要推入哪種類型的迴圈後所執行的動作。

彈出內容

內容使用 cx_popsub() 等和 cx_popblock() 進行彈出。但請注意,與 cx_pushblock 不同的是,這些函式都不會實際減少目前的內容堆疊索引;這是使用 CX_POP() 另外執行的。

彈出內容有兩種主要方式。在正常執行期間,當範圍結束時,例如 pp_leavepp_leavelooppp_leavesub 等函式會使用 cx_popfoocx_popblock 處理並彈出一個內容。另一方面,例如 pp_returnnext 等函式可能必須彈出數個範圍,直到找到子範圍或迴圈內容,而例外狀況(例如 die)則需要彈出內容,直到找到 eval 內容。這兩個動作都是由 dounwind() 完成,它能夠處理並彈出目標上方所有內容。

以下是一個內容彈出的典型範例,可以在 pp_leavesub 中找到(經過一些簡化)

U8 gimme;
PERL_CONTEXT *cx;
SV **oldsp;
OP *retop;

cx = CX_CUR();

gimme = cx->blk_gimme;
oldsp = PL_stack_base + cx->blk_oldsp; /* last arg of previous frame */

if (gimme == G_VOID)
    PL_stack_sp = oldsp;
else
    leave_adjust_stacks(oldsp, oldsp, gimme, 0);

CX_LEAVE_SCOPE(cx);
cx_popsub(cx);
cx_popblock(cx);
retop = cx->blk_sub.retop;
CX_POP(cx);

return retop;

上述步驟的順序非常特定,設計為與推入內容時相反的順序。首先要做的,是複製和/或保護任何傳回引數,並釋放目前範圍中的任何暫存。範圍結束(例如 rvalue 子範圍)通常會傳回傳回引數的 mortal 副本(與 lvalue 子範圍相反)。在彈出儲存堆疊或還原變數之前,進行此複製非常重要,否則可能會發生以下類似的問題

sub f { my $x =...; $x }  # $x freed before we get to copy it
sub f { /(...)/;    $1 }  # PL_curpm restored before $1 copied

雖然我們希望同時釋放任何暫存,但我們必須小心不要釋放任何讓傳回引數保持活動狀態的暫存;也不要釋放我們在 mortal 複製傳回引數時剛剛建立的暫存。幸運的是,leave_adjust_stacks() 能夠建立傳回引數的 mortal 副本,將引數向下移動堆疊,並只處理暫存堆疊中安全執行的那些項目。

在 void 背景下不會傳回任何參數,因此跳過呼叫 leave_adjust_stacks() 會更有效率。此外,在 void 背景下,可能會立即呼叫 nextstate op,而這會執行 FREETMPS,因此也不需要執行該動作。

下一步是彈出 savestack 項目:CX_LEAVE_SCOPE(cx) 僅定義為 LEAVE_SCOPE(cx->blk_oldsaveix)。請注意,在彈出的過程中,perl 可能會呼叫解構函式、呼叫 STORE 以取消連結變數的在地化等等。這些動作任何一個都可能導致程式死掉或呼叫 exit()。在這種情況下,會呼叫 dounwind(),而且會重新處理目前的背景堆疊架構。因此,彈出背景中的所有步驟都必須以支援重新進入的方式執行,這一點至關重要。另一個替代方案是在處理架構之前遞減 cxstack_ix,如果在過程中發生死掉的情況,或者覆寫目前的架構,就會導致記憶體外洩等問題。

CX_LEAVE_SCOPE 本身是安全的重新進入:如果在發生死掉並被 eval 捕捉之前,只有半數的 savestack 項目被彈出,那麼 dounwindpp_leaveeval 中的 CX_LEAVE_SCOPEs 會從第一個離開的地方繼續執行。

下一步是類型特定的背景處理;在這種情況下為 cx_popsub。部分內容如下

cv = cx->blk_sub.cv;
CvDEPTH(cv) = cx->blk_sub.olddepth;
cx->blk_sub.cv = NULL;
SvREFCNT_dec(cv);

其中處理的是剛執行的 CV。請注意,在遞減 CV 的參考計數之前,它會將 blk_sub.cv 設定為 null。這表示如果重新進入,CV 就會不會被釋放兩次。這也表示在從 cx_popfoo 傳回之後,您無法依賴此類型的特定欄位具有有用的值。

接下來,cx_popblock 會將所有不同的直譯器變數還原為它們之前的數值或之前的最高水位;它會擴充為

PL_markstack_ptr = PL_markstack + cx->blk_oldmarksp;
PL_scopestack_ix = cx->blk_oldscopesp;
PL_curpm         = cx->blk_oldpm;
PL_curcop        = cx->blk_oldcop;
PL_tmps_floor    = cx->blk_old_tmpsfloor;

請注意,它不會還原 PL_stack_sp;如前面所述,要還原為哪個數值取決於內容類型(特別是 for (list) {}),以及它傳回什麼參數(如果有);而這已經會由 leave_adjust_stacks() 在前面處理好。

最後,內容堆疊指標實際上會由 CX_POP(cx) 遞減。在這個點之後,目前的內容框架有可能會被其他被推入的內容覆寫。雖然像繫結和 DESTROY 這樣的東西應該可以在新的內容堆疊中運作,但最好不要假設如此。的確,在除錯建置中,CX_POP(cx) 會故意將 cx 設為 null,以偵測依舊依賴該內容框架中欄位數值的程式碼。請注意在上面的 pp_leavesub() 範例中,我們在呼叫 CX_POP 之前 取得 blk_sub.retop

重新執行內容

最後,有 cx_topblock(cx),它在將各種變數重設為它們的基本值方面,就像一個超級 nextstate。它用於像 pp_nextpp_redopp_goto 這樣的場合,在這些場合中,我們想要重新初始化範圍,而不是退出範圍。除了像 nextstate 一樣重設 PL_stack_sp 之外,它還會重設 PL_markstack_ptrPL_scopestack_ixPL_curpm。請注意,它不會執行 FREETMPS

基於區塊的運算子配置

注意:此區段描述非公開的內部 API,可能會在沒有通知的情況下變更。

Perl 的內部錯誤處理機制使用 longjmp 來實作 die(及其內部等效項)。如果這發生在詞法分析、剖析或編譯期間,我們必須確保在編譯程序中配置的任何運算子都會被釋放。(較舊的 Perl 版本無法適當地處理這個情況:當剖析失敗時,它們會洩漏儲存在 C auto 變數中且未連結到任何其他地方的運算子。)

為了處理這種情況,Perl 會使用附加到目前編譯 CV 的op slab。slab 是已配置記憶體的區塊。新的 op 會配置為 slab 的區域。如果 slab 已滿,就會建立新的 slab(並從前一個 slab 連結)。當發生錯誤且 CV 已釋放時,會釋放所有剩餘的 op。

每個 op 之前都有兩個指標:一個指向 slab 中的下一 op,另一個指向擁有它的 slab。需要下一個 op 指標,以便 Perl 可以反覆執行 slab 並釋放其所有 op。(op 結構大小不同,因此 slab 的 op 不能僅視為密集陣列。)需要 slab 指標來存取 slab 上的參考計數:當 slab 上的最後一個 op 已釋放時,slab 本身也會釋放。

slab 分配器會先將 op 放在 slab 的最後面。這會傾向於先配置 op 樹的葉節點,因此佈局有望對快取有利。此外,這表示不需要儲存 slab 的大小(請參閱下方有關 slab 大小為何不同的原因),因為 Perl 可以追蹤指標來尋找最後一個 op。

透過在配置時讓所有 op 隱式附加到 PL_compcv,並在 CV 釋放時釋放,似乎可以完全消除 slab 參考計數。這也會讓 op_free 完全略過 FreeOp,進而更快釋放 op。但這在 op 需要在其 CV 之後存續的情況下無法運作,例如重新評估。

CV 也必須在 slab 上有參考計數。有時,建立的第一個 op 會立即釋放。如果 slab 的參考計數達到 0,則會在 CV 仍指向它的情況下釋放它。

CV 使用 CVf_SLABBED 旗標來指出 CV 在 slab 上具有參考計數。當設定此旗標時,當 CvROOT 未設定時,slab 可透過 CvSTART 存取,或當其設定時,從 CvROOT 減去兩個指標 (2*sizeof(I32 *))。在編譯期間將 slab 偷偷加入 CvSTART 的這種方法的替代方法是將 xpvcv 結構擴大為另一個指標。但這會使所有 CV 都變大,即使基於 slab 的 op 釋放通常只對大量使用字串 eval 的程式有益。

當設定 CVf_SLABBED 旗標時,CV 會負責釋放 slab。如果在釋放或取消定義 CV 時未設定 CvROOT,則假設已發生編譯錯誤,因此會遍歷 op slab 並釋放所有 op。

在正常情況下,當附加根時,CV 會忘記其 slab(遞減參考計數)。因此,在釋放 op 時發生的 slab 參考計數會負責釋放 slab。在某些情況下,會告知 CV 忘記 slab(cv_forget_slab),以便在 CV 完成後 op 才能存活。

在附加根時忘記 slab 並非絕對必要,但可以避免 CvROOT 被覆寫的潛在問題。在核心和 CPAN 中到處都有程式碼,使用 CvROOT 執行各種作業,因此忘記 slab 會讓事情更健全,並避免潛在問題。

由於 CV 在標記時會取得其 slab 的所有權,因此當複製 CV 時,絕不會複製該旗標,因為一個 CV 可以釋放另一個 CV 仍指向的 slab,因為強制釋放 op 會忽略參考計數(但會斷言其看起來正確)。

為了避免區塊碎片化,已釋放的運算元會標記為已釋放,並附加到區塊的已釋放鏈(從 DBM::Deep 偷來的點子)。這些已釋放的運算元會在可能的情況下重複使用。不重複使用已釋放的運算元會比較簡單,但會導致具有大型 if (DEBUG) {...} 區塊的程式記憶體使用量大幅增加。

SAVEFREEOP 在這個架構下會出現一些問題。有時它會導致運算元在其 CV 之後被釋放。如果 CV 強制釋放其區塊上的運算元和區塊本身,那麼我們就會對已釋放的區塊進行調整。將 SAVEFREEOP 設定為無作用並無幫助,因為有時運算元可以在沒有編譯錯誤的情況下被儲存釋放,因此運算元永遠不會被釋放。它會在區塊上保留一個參考計數,因此整個區塊會外洩。因此,SAVEFREEOP 現在會在運算元上設定一個特殊標記(->op_savefree)。在編譯錯誤後強制釋放運算元不會釋放任何如此標記的運算元。

由於許多程式碼會建立僅包含幾個運算元的小型子常式,而且由於一個巨大的區塊對這些運算元來說會相當累贅,因此第一個區塊總是會很小。為了避免為單一 CV 分配過多區塊,每個後續區塊都會是前一個區塊大小的兩倍。

Smartmatch 預期能夠在執行階段分配一個運算元、執行它,然後將它丟棄。為了讓它運作,當未設定 PL_compcv 時,運算元會簡單地透過 malloc 分配。因此,所有區塊分配的運算元都會標記為此類運算元(->op_slabbed),以將它們與透過 malloc 分配的運算元區分開來。

作者

在 1997 年 5 月之前,此文件由 Jeff Okamoto <okamoto@corp.hp.com> 維護。現在由 Perl 5 Porters <perl5-porters@perl.org> 作為 Perl 本身的一部分來維護。

在 Dean Roehrich、Malcolm Beattie、Andreas Koenig、Paul Hudson、Ilya Zakharevich、Paul Marquess、Neil Bowers、Matthew Green、Tim Bunce、Spider Boardman、Ulrich Pfeifer、Stephen McCamant 和 Gurusamy Sarathy 的大量協助和建議下完成。

另請參閱

perlapiperlinternperlxsperlembed