目錄

名稱

perlxstypemap - Perl XS C/Perl 類型對應

說明

您越思考在兩種語言之間的介面,您就會越了解大多數程式設計師的努力都必須花在轉換兩種語言的原生資料結構。這比其他問題(例如不同的呼叫慣例)更重要,因為問題空間更大。將資料塞進記憶體的方法比實作函數呼叫的方法多很多。

Perl XS 嘗試解決此問題的方法是類型對應的概念。在抽象層級中,Perl XS 類型對應只是一個將特定的 Perl 資料結構轉換為特定的 C 資料結構,反之亦然的食譜。由於某些 C 類型彼此足夠相似,因此可以用相同的邏輯進行轉換,因此 XS 類型對應會以一個唯一的識別碼表示,在本文檔中稱為XS 類型。然後,您可以告訴 XS 編譯器要使用相同的 XS 類型對應來對應多個 C 類型。

在您的 XS 程式碼中,當您使用 C 類型定義參數或使用 CODE:OUTPUT: 區段與 XSUB 的 C 傳回類型一起使用時,類型對應機制會讓這變得容易。

類型對應的剖析

在更實際的方面,類型對應是程式碼片段的集合,由 xsubpp 編譯器用於將 C 函式參數和值對應到 Perl 值。類型對應檔案可能包含標記為 TYPEMAPINPUTOUTPUT 的三個區段。未標記的初始區段假設為 TYPEMAP 區段。INPUT 區段告訴編譯器如何將 Perl 值轉換為特定 C 類型的變數。OUTPUT 區段告訴編譯器如何將特定 C 類型的值轉換為 Perl 可以理解的值。TYPEMAP 區段告訴編譯器哪一個 INPUT 和 OUTPUT 程式碼片段應使用來將給定的 C 類型對應到 Perl 值。標記為 TYPEMAPINPUTOUTPUT 的區段必須從一行中的第一個欄位開始,且必須使用大寫字母。

每種類型的區段都可以出現任意次數,而且不必出現。例如,類型對應通常會缺少 INPUTOUTPUT 區段,如果它只需要將其他 C 類型與核心 XS 類型(例如 T_PTROBJ)關聯即可。以井號 # 開頭的行在 TYPEMAP 區段中被視為註解並忽略,但在 INPUTOUTPUT 中被視為重要。空白行通常會被忽略。

傳統上,類型對應需要寫入一個獨立的檔案,在 CPAN 發行版中慣例稱為 typemap。使用 perl 5.16 附帶的 ExtUtils::ParseXS(XS 編譯器)版本 3.12 或更新版本,類型對應也可以使用類似 HERE 文件的語法直接嵌入 XS 程式碼中

TYPEMAP: <<HERE
...
HERE

其中 HERE 可以用其他識別碼取代,就像一般的 Perl HERE 文件一樣。以下關於類型對應文字格式的所有詳細資訊仍然有效。

TYPEMAP 區段應包含每行一對 C 類型和 XS 類型,如下所示。核心類型對應檔案中的範例

TYPEMAP
# all variants of char* is handled by the T_PV typemap
char *          T_PV
const char *    T_PV
unsigned char * T_PV
...

INPUTOUTPUT 區段有相同的格式,也就是說,每一行開頭沒有縮排的會分別開始一個新的輸入或輸出對應。新的輸入或輸出對應必須以要對應的 XS 類型的名稱開始,放在一行,接著在下一行縮排,實作它的程式碼。範例

INPUT
T_PV
  $var = ($type)SvPV_nolen($arg)
T_PTR
  $var = INT2PTR($type,SvIV($arg))

我們稍後會說明那些看起來像 Perl 的變數的意義。

最後,以下是將 char * 類型的 C 字串對應到 Perl 純量/字串的完整 typemap 檔案範例

TYPEMAP
char *  T_PV

INPUT
T_PV
  $var = ($type)SvPV_nolen($arg)

OUTPUT
T_PV
  sv_setpv((SV*)$arg, $var);

以下是更複雜的範例:假設你想要將 struct netconfig 祝福成 Net::Config 類別。一種方法是使用底線 (_) 來區分套件名稱,如下所示

typedef struct netconfig * Net_Config;

然後提供一個 typemap 項目 T_PTROBJ_SPECIAL,將底線對應到雙冒號 (::),並宣告 Net_Config 是那個類型

TYPEMAP
Net_Config      T_PTROBJ_SPECIAL

INPUT
T_PTROBJ_SPECIAL
  if (sv_derived_from($arg, \"${(my $ntt=$ntype)=~s/_/::/g;\$ntt}\")){
    IV tmp = SvIV((SV*)SvRV($arg));
    $var = INT2PTR($type, tmp);
  }
  else
    croak(\"$var is not of type ${(my $ntt=$ntype)=~s/_/::/g;\$ntt}\")

OUTPUT
T_PTROBJ_SPECIAL
  sv_setref_pv($arg, \"${(my $ntt=$ntype)=~s/_/::/g;\$ntt}\",
               (void*)$var);

INPUT 和 OUTPUT 區段會動態地將底線替換成雙冒號,產生預期的效果。這個範例展示了 typemap 設施的一些強大功能和通用性。

INT2PTR 巨集(在 perl.h 中定義)將整數轉換成特定類型的指標,處理整數和指標可能不同的長度。還有 PTR2IVPTR2UVPTR2NV 巨集,用於反向對應,這在 OUTPUT 區段中可能會很有用。

typemap 檔案在你的發行套件中的角色

Perl 原始碼中的 lib/ExtUtils 目錄中的預設 typemap 包含許多 Perl 擴充套件可以用到的有用類型。有些擴充套件會定義額外的 typemap,並將它們放在自己的目錄中。這些額外的 typemap 可能會參考主 typemap 中的 INPUT 和 OUTPUT 對應。xsubpp 編譯器會允許擴充套件自己的 typemap 覆寫預設 typemap 中的任何對應。除了使用額外的 typemap 檔案,typemap 也可以使用類似 here 文件的語法,內嵌在 XS 中。請參閱 TYPEMAP: XS 關鍵字的說明文件。

對於 CPAN 發行套件,你可以假設 Perl 核心定義的 XS 類型已經可以使用了。此外,核心 typemap 有大量的 C 類型的預設 XS 類型。例如,如果你只是從你的 XSUB 傳回一個 char *,核心 typemap 會將這個 C 類型關聯到 T_PV XS 類型。這表示你的 C 字串會複製到從你的 XSUB 傳回給 Perl 的新純量的 PV(指標值)槽位中。

如果您使用 XS 開發 CPAN 套件,您可以將自己的檔案稱為 typemap 加入套件中。該檔案可能包含將特定於您的程式碼的類型進行對應,或覆寫核心 typemap 檔案對應的常見 C 類型的 typemap。

在 CPAN 套件之間分享 typemap

從 ExtUtils::ParseXS 版本 3.13_01 開始(附帶 perl 5.16 和更新版本),在多個 CPAN 套件之間分享 typemap 程式碼相當容易。一般來說,分享方式是將其作為提供特定 API 的模組,並讓依賴模組宣告其為建置時期需求,然後將 typemap 匯入 XS。CPAN 上此類型的 typemap 分享模組範例為 ExtUtils::Typemaps::Basic。讓該模組的 typemap 在您的程式碼中可用的兩個步驟:

撰寫 typemap 項目

每個 INPUT 或 OUTPUT typemap 項目都是一個雙引號包住的 Perl 字串,在特定變數存在的情況下會評估該字串,以取得對應特定 C 類型的最終 C 程式碼。

這表示您可以在 typemap(C)程式碼中使用 ${ 在此評估為純量參考的 perl 程式碼 } 等建構,來嵌入 Perl 程式碼。常見的用例是在使用 ALIAS XS 功能時產生參考真實函數名稱的錯誤訊息。

${ $ALIAS ? \q[GvNAME(CvGV(cv))] : \qq[\"$pname\"] }

有關許多 typemap 範例,請參閱核心 typemap 檔案,該檔案可以在 perl 原始程式碼樹的 lib/ExtUtils/typemap 中找到。

可內插到 typemap 的 Perl 變數如下:

核心 Typemap 的完整清單

每個 C 類型都由 typemap 檔案中的項目表示,該項目負責將 perl 變數(SV、AV、HV、CV 等)轉換為該類型,並從該類型轉換為 perl 變數。下列各節列出 perl 預設附帶的所有 XS 類型。

T_SV

這僅將 Perl 變數(SV*)的 C 表示傳入和傳出 XS 層。如果 C 程式碼想要直接處理 Perl 變數,可以使用這個。

T_SVREF

用於傳入和傳回對 SV 的參考。

請注意,此 typemap 在傳回對 SV* 的參考時不會遞減參考計數。另請參閱:T_SVREF_REFCOUNT_FIXED

T_SVREF_FIXED

用於傳入和傳回對 SV 的參考。這是 T_SVREF 的固定變體,在傳回對 SV* 的參考時會適當地遞減參考計數。在 perl 5.15.4 中引入。

T_AVREF

從 perl 層面來看,這是對 perl 陣列的參考。從 C 層面來看,這是指向 AV 的指標。

請注意,此 typemap 在傳回 AV* 時不會遞減參考計數。另請參閱:T_AVREF_REFCOUNT_FIXED

T_AVREF_REFCOUNT_FIXED

從 perl 層面來看,這是對 perl 陣列的參考。從 C 層面來看,這是指向 AV 的指標。這是 T_AVREF 的固定變體,在傳回對 AV* 的參考時會適當地遞減參考計數。在 perl 5.15.4 中引入。

T_HVREF

從 perl 層面來看,這是對 perl 雜湊的參考。從 C 層面來看,這是指向 HV 的指標。

請注意,此 typemap 在傳回 HV* 時不會遞減參考計數。另請參閱:T_HVREF_REFCOUNT_FIXED

T_HVREF_REFCOUNT_FIXED

從 perl 層面來看,這是對 perl 雜湊的參考。從 C 層面來看,這是指向 HV 的指標。這是 T_HVREF 的固定變體,在傳回對 HV* 的參考時會適當地遞減參考計數。在 perl 5.15.4 中引入。

T_CVREF

從 perl 層面來看,這是對 perl 子常式的參考(例如 $sub = sub { 1 };)。從 C 層面來看,這是指向 CV 的指標。

請注意,此 typemap 在傳回 HV* 時不會遞減參考計數。另請參閱:T_HVREF_REFCOUNT_FIXED

T_CVREF_REFCOUNT_FIXED

從 perl 層面來看,這是對 perl 子常式的參考(例如 $sub = sub { 1 };)。從 C 層面來看,這是指向 CV 的指標。

這是 T_HVREF 的固定變異,在傳回 HV* 時適當地遞減參照計數。在 perl 5.15.4 中引入。

T_SYSRET

T_SYSRET 類型映射用於處理來自系統呼叫的傳回值。它僅在從 C 傳遞值到 perl 時才有意義(沒有從 Perl 傳遞系統傳回值到 C 的概念)。

系統呼叫在發生錯誤時傳回 -1(使用 ERRNO 設定原因),在成功時(通常)傳回 0。如果傳回值為 -1,這個類型映射傳回 undef。如果傳回值不為 -1,這個類型映射會將 0(perl false)轉換為「0 但為 true」(這是 perl true)或傳回值本身,以表示命令成功執行。

POSIX 模組大量使用這個類型。

T_UV

一個未簽署的整數。

T_IV

一個有簽署的整數。傳遞到 C 時會轉換為所需的整數類型,傳遞回 Perl 時會轉換為 IV。

T_INT

一個有簽署的整數。這個類型映射會將 Perl 值轉換為本機整數類型(目前平台上的 int 類型)。在將值傳回 perl 時,它的處理方式與 T_IV 相同。

它的行為與在 XS 中使用 T_IV 的 int 類型相同。

T_ENUM

一個列舉值。用於從 C 傳遞列舉元件。沒有理由將列舉值傳遞到 C,因為它儲存在 perl 內部的 IV 中。

T_BOOL

一個布林類型。這可以用於傳遞 true 和 false 值到 C 和從 C 傳遞值。

T_U_INT

這是用於未簽署整數。它等於使用 T_UV,但會明確地將變數轉換為 unsigned int 類型。unsigned int 的預設類型為 T_UV。

T_SHORT

短整數。這等於 T_IV,但會明確地將傳回值轉換為 short 類型。short 的預設類型映射為 T_IV。

T_U_SHORT

未簽署的短整數。這等於 T_UV,但會明確地將傳回值轉換為 unsigned short 類型。unsigned short 的預設類型映射為 T_UV。

T_U_SHORT 用於標準類型映射中的 U16 類型。

T_LONG

長整數。這等於 T_IV,但會明確地將傳回值轉換為 long 類型。long 的預設類型映射為 T_IV。

T_U_LONG

未簽署的長整數。這等於 T_UV,但明確地將回傳值轉型為類型 unsigned longunsigned long 的預設類型圖為 T_UV。

在標準類型圖中,T_U_LONG 用於類型 U32

T_CHAR

單一 8 位元字元。

T_U_CHAR

未簽署的位元組。

T_FLOAT

浮點數。此類型圖保證回傳一個轉型為 float 的變數。

T_NV

Perl 浮點數。類似於 T_IV 和 T_UV,其中回傳類型轉型為所要求的數字類型,而不是轉型為特定類型。

T_DOUBLE

雙精度浮點數。此類型圖保證回傳一個轉型為 double 的變數。

T_PV

字串 (char *)。

T_PTR

記憶體位址 (指標)。通常與 void * 類型相關聯。

T_PTRREF

類似於 T_PTR,但指標儲存在一個純量中,而該純量的參考則回傳給呼叫者。這可以用於向程式設計師隱藏實際指標值,因為通常不需要直接從 perl 中取得該值。

類型圖檢查純量參考是否從 perl 傳遞到 XS。

T_PTROBJ

類似於 T_PTRREF,但參考會被賜福為一個類別。這允許指標作為物件使用。最常使用於處理 C 結構。類型圖檢查傳遞到 XS 常式的 perl 物件是否屬於正確的類別 (或子類別的一部分)。

指標會被賜福為一個類別,該類別衍生自指標類型的名稱,但名稱中的所有 '*' 都替換為 'Ptr'。

僅針對 DESTROY XSUB,T_PTROBJ 會最佳化為 T_PTRREF。這表示會略過類別檢查。

T_REF_IV_REF

尚未

T_REF_IV_PTR

類似於 T_PTROBJ,其中指標會被賜福為純量物件。不同之處在於,當物件傳回 XS 時,它必須屬於正確的類型 (不支援繼承),而 T_PTROBJ 支援繼承。

指標會被賜福為一個類別,該類別衍生自指標類型的名稱,但名稱中的所有 '*' 都替換為 'Ptr'。

僅針對 DESTROY XSUB,T_REF_IV_PTR 會最佳化為 T_PTRREF。這表示會略過類別檢查。

T_PTRDESC

尚未

T_REFREF

類似於 T_PTRREF,但儲存在參考標量中的指標會被解除參考並複製到輸出變數。這表示 T_REFREF 對 T_PTRREF 的關係,就像 T_OPAQUE 對 T_OPAQUEPTR 的關係。一切都清楚了嗎?

只有此輸入部分已實作(Perl 至 XSUB),且核心或 CPAN 中沒有已知的使用者。

T_REFOBJ

類似於 T_REFREF,但它會進行嚴格的類型檢查(不支援繼承)。

僅針對 DESTROY XSUB,T_REFOBJ 會最佳化為 T_REFREF。這表示會略過類別檢查。

T_OPAQUEPTR

這可用於儲存 SV 字串元件中的位元組。在此,資料的表示與 Perl 無關,而位元組本身只儲存在 SV 中。假設 C 變數是一個指標(位元組會從該記憶體位置複製)。如果指標指向由 8 個位元組表示的內容,則這 8 個位元組會儲存在 SV 中(且 length() 會回報 8 的值)。此項目類似於 T_OPAQUE。

原則上,unpack() 指令可用於將位元組轉換回數字(如果底層類型已知為數字)。

此項目可用於儲存 C 結構(要複製的位元組數目會使用 C sizeof 函數計算),且可用作 T_PTRREF 的替代方案,而不用擔心記憶體外洩(因為 Perl 會清除 SV)。

T_OPAQUE

這可用於儲存 SV 字串部分中非指標類型的資料。它類似於 T_OPAQUEPTR,但類型圖會直接擷取指標,而不是假設它會提供。例如,如果使用 T_OPAQUE 而不是 T_IV 將整數匯入 Perl,則表示整數的底層位元組會儲存在 SV 中,但實際整數值將不可用。也就是說,資料對 Perl 來說是不透明的。

如果已知位元組串流的底層類型,則可以使用 unpack 函數擷取資料。

T_OPAQUE 支援輸入和輸出簡單類型。如果指標是可以接受的,則 T_OPAQUEPTR 可用於將這些位元組傳回 C。

隱式陣列

xsubpp 支援一種特殊語法,用於將封裝的 C 陣列傳回 Perl。如果 XS 回傳類型指定為

array(type, nelem)

xsubpp 會將 nelem * sizeof(type) 位元組的內容從 RETVAL 複製到 SV,並將其推入堆疊。這僅在編譯時已知要傳回的項目數目時才真正有用,而且您不介意在 SV 中有一串位元組。使用 T_ARRAY 將可變數目的引數推入回傳堆疊(但它們不會封裝為單一字串)。

這類似於使用 T_OPAQUEPTR,但可用於處理多個元素。

T_PACKED

呼叫使用者提供的函式進行轉換。對於 OUTPUT(XSUB 到 Perl),會呼叫名為 XS_pack_$ntype 的函式,並傳入輸出 Perl 純量和要從中轉換的 C 變數。$ntype 是要對應到 Perl 的正規化 C 類型。正規化表示所有 * 都已替換為字串 Ptr。會略過函式的回傳值。

相反地,對於 INPUT(Perl 到 XSUB)對應,會呼叫名為 XS_unpack_$ntype 的函式,並將輸入 Perl 純量作為引數,而回傳值會強制轉型為對應的 C 類型,並指派給輸出 C 變數。

類型對應結構 foo_t * 的範例轉換函式可能是

static void
XS_pack_foo_tPtr(SV *out, foo_t *in)
{
  dTHX; /* alas, signature does not include pTHX_ */
  HV* hash = newHV();
  hv_stores(hash, "int_member", newSViv(in->int_member));
  hv_stores(hash, "float_member", newSVnv(in->float_member));
  /* ... */

  /* mortalize as thy stack is not refcounted */
  sv_setsv(out, sv_2mortal(newRV_noinc((SV*)hash)));
}

從 Perl 到 C 的轉換留待讀者練習,但原型會是

static foo_t *
XS_unpack_foo_tPtr(SV *in);

您不必使用必須使用 dTHX 擷取執行緒內容的實際 C 函式,您可以定義相同名稱的巨集並避免開銷。此外,請記住可能要釋放 XS_unpack_foo_tPtr 所配置的記憶體。

T_PACKEDARRAY

T_PACKEDARRAY 類似於 T_PACKED。事實上,INPUT(Perl 到 XSUB)類型對應是相同的,但 OUTPUT 類型對應會將額外的引數傳遞給 XS_pack_$ntype 函式。這個第三個參數表示輸出中的元素數目,以便函式可以合理地處理 C 陣列。變數需要由使用者宣告,且必須命名為 count_$ntype,其中 $ntype 是如上所述的正規化 C 類型名稱。函式的簽章會針對上述範例和 foo_t **

static void
XS_pack_foo_tPtrPtr(SV *out, foo_t *in, UV count_foo_tPtrPtr);

就類型映射而言,第三個參數的類型是任意的。它只需要與宣告的變數一致即可。

當然,除非您知道 XSUB 中 sometype ** C 陣列中的元素數量,否則 foo_t ** XS_unpack_foo_tPtrPtr(...) 的傳回值將難以解碼。由於細節都取決於 XS 作者(類型映射使用者),因此有數種解決方案,但沒有一個特別優雅。最常見的解決方案是為 N+1 個指標分配記憶體,並將 NULL 指定給第 (N+1) 個指標以利於反覆運算。

或者,首先為您的目的使用自訂類型映射可能是較好的選擇。

T_DATAUNIT

尚未

T_CALLBACK

尚未

T_ARRAY

這用於將 Perl 參數清單轉換為 C 陣列,以及將 C 陣列的內容推送到 Perl 參數堆疊中。

通常的呼叫簽章為

@out = array_func( @in );

陣列之前清單中可以出現任意數量的參數,但輸入和輸出陣列必須是清單中的最後元素。

當用於將 Perl 清單傳遞到 C 時,XS 寫作者必須提供一個函式(以陣列類型命名,但將 '*' 替換為 'Ptr')來分配儲存清單所需的記憶體。應傳回一個指標。XS 寫作者負責在函式結束時釋放記憶體。變數 ix_$var 設定為新陣列中的元素數量。

當將 C 陣列傳回 Perl 時,XS 寫作者必須提供一個名為 size_$var 的整數變數,其中包含陣列中的元素數量。這用於確定應將多少個元素推送到傳回參數堆疊中。輸入時不需要這樣做,因為 Perl 在呼叫常式時知道堆疊中有多少個參數。通常這個變數會稱為 size_RETVAL

此外,每個元素的類型是由陣列的類型決定的。如果陣列使用類型 intArray *,xsubpp 會自動找出它包含類型為 int 的變數,並使用該類型映射條目來執行每個元素的複製。所有指標 '*' 和 'Array' 標記都從名稱中移除,以確定子類型。

T_STDIO

這用於使用 FILE * 結構將 Perl 檔案控制代碼傳遞到 C 和從 C 傳遞到 Perl。

T_INOUT

這用於使用 PerlIO * 結構將 Perl 檔案句柄傳遞到 C 和從 C 傳遞到 Perl。檔案句柄可用於讀取和寫入。這對應於 +< 模式,另請參閱 T_IN 和 T_OUT。

請參閱 perliol 以進一步瞭解 Perl IO 抽象層。Perl 必須使用 -Duseperlio 建置。

沒有檢查來斷言從 Perl 傳遞到 C 的檔案句柄是使用正確的 open() 模式建立的。

提示:perlxstut 教學課程很好地涵蓋了 T_INOUT、T_IN 和 T_OUT XS 類型。

T_IN

與 T_INOUT 相同,但從 C 返回到 Perl 的檔案句柄只能用於讀取(模式 <)。

T_OUT

與 T_INOUT 相同,但從 C 返回到 Perl 的檔案句柄設定為使用開啟模式 +>