perlxstypemap - Perl XS C/Perl 类型映射
您越深入地思考两种语言之间的接口,您就越会意识到,大多数程序员的工作都必须投入到转换两种语言的原生数据结构之间。这比其他问题(如不同的调用约定)更重要,因为问题空间要大得多。将数据塞入内存的方法比实现函数调用的方法要多得多。
Perl XS 对此问题的解决方案是类型映射的概念。从抽象的角度来看,Perl XS 类型映射只不过是将特定 Perl 数据结构转换为特定 C 数据结构的食谱,反之亦然。由于 C 类型可能足够相似,以至于可以使用相同的逻辑进行转换,因此 XS 类型映射由一个唯一的标识符表示,在本文件中称为 **XS 类型**。然后,您可以告诉 XS 编译器将多个 C 类型映射到相同的 XS 类型映射。
在您的 XS 代码中,当您使用 C 类型定义参数,或者当您使用 CODE:
和 OUTPUT:
部分以及 XSUB 的 C 返回类型时,类型映射机制将使这变得容易。
从更实用的角度来说,typemap 是一个代码片段集合,由 **xsubpp** 编译器用来将 C 函数参数和值映射到 Perl 值。typemap 文件可以包含三个部分,分别标记为 `TYPEMAP`、`INPUT` 和 `OUTPUT`。未标记的初始部分被认为是 `TYPEMAP` 部分。`INPUT` 部分告诉编译器如何将 Perl 值转换为特定 C 类型的变量。`OUTPUT` 部分告诉编译器如何将特定 C 类型的值转换为 Perl 可以理解的值。`TYPEMAP` 部分告诉编译器应该使用哪些 `INPUT` 和 `OUTPUT` 代码片段来将给定的 C 类型映射到 Perl 值。部分标签 `TYPEMAP`、`INPUT` 或 `OUTPUT` 必须以大写字母开头,并单独占一行,且必须位于第一列。
每种类型的部分可以出现任意次数,也可以完全不出现。例如,如果 typemap 只需要将额外的 C 类型与核心 XS 类型(如 T_PTROBJ)关联,则通常会缺少 `INPUT` 和 `OUTPUT` 部分。以井号 `#` 开头的行被视为注释,在 `TYPEMAP` 部分中被忽略,但在 `INPUT` 和 `OUTPUT` 部分中被视为有效。空白行通常被忽略。
传统上,typemap 需要写入一个单独的文件,通常在 CPAN 发行版中称为 `typemap`。使用 ExtUtils::ParseXS(XS 编译器)3.12 或更高版本(随 perl 5.16 提供),typemap 也可以直接嵌入到 XS 代码中,使用类似于 HERE-doc 的语法
TYPEMAP: <<HERE
...
HERE
其中 `HERE` 可以用其他标识符替换,就像使用普通的 Perl HERE-doc 一样。以下关于 typemap 文本格式的所有细节仍然有效。
`TYPEMAP` 部分应包含每行一对 C 类型和 XS 类型,如下所示。来自核心 typemap 文件的示例
TYPEMAP
# all variants of char* is handled by the T_PV typemap
char * T_PV
const char * T_PV
unsigned char * T_PV
...
`INPUT` 和 `OUTPUT` 部分具有相同的格式,即每个未缩进的行分别开始一个新的输入或输出映射。新的输入或输出映射必须以要映射的 XS 类型名称开头,单独占一行,然后是实现它的代码,在接下来的几行中缩进。示例
INPUT
T_PV
$var = ($type)SvPV_nolen($arg)
T_PTR
$var = INT2PTR($type,SvIV($arg))
我们稍后会解释这些看起来像 Perl 变量的含义。
最后,这是一个将 C 字符串(char *
类型)映射到 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 中定义)将一个整数转换为给定类型的指针,并处理整数和指针可能不同的大小。还有 PTR2IV
、PTR2UV
、PTR2NV
宏,用于反向映射,这在 OUTPUT 部分可能很有用。
Perl 源代码的 lib/ExtUtils 目录中的默认 typemap 包含许多有用的类型,这些类型可以被 Perl 扩展使用。一些扩展定义了额外的 typemap,它们保存在自己的目录中。这些额外的 typemap 可以引用主 typemap 中的 INPUT 和 OUTPUT 映射。xsubpp 编译器将允许扩展自己的 typemap 覆盖默认 typemap 中的任何映射。除了使用额外的 typemap 文件之外,typemap 可以使用类似于 heredoc 的语法直接嵌入到 XS 中。请参阅有关 TYPEMAP:
XS 关键字的文档。
对于 CPAN 发行版,你可以假设 Perl 核心定义的 XS 类型已经可用。此外,核心 typemap 对大量的 C 类型有默认的 XS 类型。例如,如果你只是从你的 XSUB 返回一个 char *
,核心 typemap 将把这个 C 类型与 T_PV XS 类型关联起来。这意味着你的 C 字符串将被复制到一个新标量的 PV(指针值)槽中,该标量将从你的 XSUB 返回到 Perl。
如果您正在使用 XS 开发 CPAN 发行版,您可以添加一个名为 typemap 的文件到发行版中。该文件可能包含类型映射,这些映射要么映射特定于您的代码的类型,要么覆盖核心类型映射文件对常见 C 类型的映射。
从 ExtUtils::ParseXS 版本 3.13_01(随 perl 5.16 及更高版本提供)开始,在多个 CPAN 发行版之间共享类型映射代码变得相当容易。一般思路是将其作为一个提供特定 API 的模块进行共享,并让依赖模块将其声明为构建时需求,并将类型映射导入 XS。CPAN 上此类类型映射共享模块的一个示例是 ExtUtils::Typemaps::Basic
。要使该模块的类型映射在您的代码中可用,需要两个步骤。
在 Makefile.PL
(使用 BUILD_REQUIRES
)或 Build.PL
(使用 build_requires
)中声明 ExtUtils::Typemaps::Basic
作为构建时依赖项。
在 XS 文件的 XS 部分中包含以下行:(不要换行)
INCLUDE_COMMAND: $^X -MExtUtils::Typemaps::Cmd
-e "print embeddable_typemap(q{Basic})"
每个 INPUT 或 OUTPUT 类型映射条目都是一个双引号的 Perl 字符串,它将在某些变量存在的情况下进行评估,以获取用于映射特定 C 类型的最终 C 代码。
这意味着您可以使用诸如 ${ perl code that evaluates to scalar reference here }
之类的结构,在类型映射(C)代码中嵌入 Perl 代码。一个常见的用例是生成引用真实函数名的错误消息,即使使用 ALIAS XS 功能也是如此。
${ $ALIAS ? \q[GvNAME(CvGV(cv))] : \qq[\"$pname\"] }
有关许多类型映射示例,请参阅核心类型映射文件,该文件可以在 perl 源代码树中的 lib/ExtUtils/typemap 中找到。
可用于插值到类型映射中的 Perl 变量如下:
$var - 输入或输出变量的名称,例如 RETVAL 用于返回值。
$type - 参数的原始 C 类型,任何 :
都被替换为 _
。例如,对于类型 Foo::Bar
,$type 为 Foo__Bar
。
$ntype - 提供的类型,其中 *
被替换为 Ptr
。例如,对于类型 Foo*
,$ntype 为 FooPtr
。
$arg - 参数输入或输出到的堆栈条目,例如 ST(0)
。
$argoff - 参数的堆栈偏移量。即第一个参数为 0,依此类推。
$pname - XSUB 的完整名称,包括 PACKAGE
名称,并去除任何 PREFIX
。这是非 ALIAS 名称。
$Package - 由最近的 PACKAGE
关键字指定的包。
$ALIAS - 如果当前 XSUB 具有使用 ALIAS
声明的任何别名,则为非零值。
每个 C 类型在 typemap 文件中都有一个条目,该条目负责将 perl 变量(SV、AV、HV、CV 等)转换为该类型,反之亦然。以下部分列出了 perl 默认提供的所有 XS 类型。
这只是将 Perl 变量的 C 表示形式(一个 SV*)传入和传出 XS 层。如果 C 代码想要直接处理 Perl 变量,可以使用它。
用于传入和返回对 SV 的引用。
请注意,此 typemap 在将对 SV* 的引用返回时不会递减引用计数。另请参见:T_SVREF_REFCOUNT_FIXED
用于传入和返回对 SV 的引用。这是 T_SVREF 的固定变体,在将对 SV* 的引用返回时会适当地递减引用计数。在 perl 5.15.4 中引入。
从 perl 级别来看,这是对 perl 数组的引用。从 C 级别来看,这是指向 AV 的指针。
请注意,此 typemap 在将 AV* 返回时不会递减引用计数。另请参见:T_AVREF_REFCOUNT_FIXED
从 perl 级别来看,这是对 perl 数组的引用。从 C 级别来看,这是指向 AV 的指针。这是 T_AVREF 的固定变体,在将 AV* 返回时会适当地递减引用计数。在 perl 5.15.4 中引入。
从 perl 级别来看,这是对 perl 哈希的引用。从 C 级别来看,这是指向 HV 的指针。
请注意,此 typemap 在将 HV* 返回时不会递减引用计数。另请参见:T_HVREF_REFCOUNT_FIXED
从 perl 级别来看,这是对 perl 哈希的引用。从 C 级别来看,这是指向 HV 的指针。这是 T_HVREF 的固定变体,在将 HV* 返回时会适当地递减引用计数。在 perl 5.15.4 中引入。
从 perl 级别来看,这是对 perl 子例程的引用(例如 $sub = sub { 1 };)。从 C 级别来看,这是指向 CV 的指针。
请注意,此 typemap 在将 HV* 返回时不会递减引用计数。另请参见:T_HVREF_REFCOUNT_FIXED
从 perl 级别来看,这是对 perl 子例程的引用(例如 $sub = sub { 1 };)。从 C 级别来看,这是指向 CV 的指针。
这是 T_HVREF 的固定变体,在返回 HV* 时会适当地递减引用计数。在 perl 5.15.4 中引入。
T_SYSRET 类型映射用于处理系统调用的返回值。它仅在将值从 C 传递到 perl 时有意义(从 Perl 到 C 传递系统返回值的概念不存在)。
系统调用在出错时返回 -1(使用 ERRNO 设置原因),在成功时(通常)返回 0。如果返回值为 -1,则此类型映射返回 undef
。如果返回值不为 -1,则此类型映射将 0(perl false)转换为“0 但为真”(即 perl true)或返回该值本身,以指示命令成功。
POSIX 模块广泛使用此类型。
无符号整数。
有符号整数。在传递到 C 时,它会被强制转换为所需的整数类型,并在传递回 Perl 时转换为 IV。
有符号整数。此类型映射将 Perl 值转换为本地整数类型(当前平台上的 int
类型)。在将值返回到 perl 时,它会以与 T_IV 相同的方式进行处理。
它的行为与在 XS 中使用 int
类型和 T_IV 相同。
枚举值。用于从 C 传输枚举组件。没有理由将枚举值传递到 C,因为它在 perl 内部以 IV 形式存储。
布尔类型。这可用于将真假值传递到 C 和从 C 传递。
这是用于无符号整数。它等效于使用 T_UV,但显式地将变量强制转换为 unsigned int
类型。unsigned int
的默认类型为 T_UV。
短整数。它等效于 T_IV,但显式地将返回值强制转换为 short
类型。short
的默认类型映射为 T_IV。
无符号短整型。这等效于 T_UV,但显式地将返回值转换为 unsigned short
类型。unsigned short
的默认类型映射为 T_UV。
T_U_SHORT 用于标准类型映射中的 U16
类型。
长整型。这等效于 T_IV,但显式地将返回值转换为 long
类型。long
的默认类型映射为 T_IV。
无符号长整型。这等效于 T_UV,但显式地将返回值转换为 unsigned long
类型。unsigned long
的默认类型映射为 T_UV。
T_U_LONG 用于标准类型映射中的 U32
类型。
单个 8 位字符。
无符号字节。
浮点数。此类型映射保证返回转换为 float
类型的变量。
Perl 浮点数。类似于 T_IV 和 T_UV,返回值类型转换为请求的数字类型,而不是特定类型。
双精度浮点数。此类型映射保证返回转换为 double
类型的变量。
字符串 (char *)。
内存地址 (指针)。通常与 void *
类型相关联。
类似于 T_PTR,但指针存储在标量中,对该标量的引用返回给调用者。这可用于隐藏实际指针值,因为通常不需要从 perl 内部直接使用它。
类型映射检查从 perl 传递到 XS 的标量引用。
类似于 T_PTRREF,但引用被祝福到一个类中。这允许指针用作对象。最常用于处理 C 结构体。类型映射检查传递到 XS 例程的 perl 对象是否属于正确的类(或子类的一部分)。
指针被祝福到一个类中,该类派生自指针类型的名称,但名称中的所有 '*' 都被替换为 'Ptr'。
仅对于 DESTROY
XSUB,T_PTROBJ 被优化为 T_PTRREF。这意味着类检查被跳过。
尚未实现
类似于 T_PTROBJ,指针被祝福为一个标量对象。区别在于,当对象被传回 XS 时,它必须是正确的类型(不支持继承),而 T_PTROBJ 支持继承。
指针被祝福到一个类中,该类派生自指针类型的名称,但名称中的所有 '*' 都被替换为 'Ptr'。
仅对于 DESTROY
XSUB,T_REF_IV_PTR 被优化为 T_PTRREF。这意味着类检查被跳过。
尚未实现
类似于 T_PTRREF,但存储在引用标量中的指针被解引用并复制到输出变量。这意味着 T_REFREF 与 T_PTRREF 的关系类似于 T_OPAQUE 与 T_OPAQUEPTR 的关系。清楚了吗?
目前只实现了 INPUT 部分(Perl 到 XSUB),并且在核心或 CPAN 上没有已知的用户。
类似于 T_REFREF,但它执行严格的类型检查(不支持继承)。
仅对于 DESTROY
XSUB,T_REFOBJ 被优化为 T_REFREF。这意味着类检查被跳过。
这可以用来在 SV 的字符串组件中存储字节。这里数据的表示形式与 Perl 无关,字节本身只是存储在 SV 中。假设 C 变量是一个指针(字节从该内存位置复制)。如果指针指向由 8 个字节表示的内容,那么这 8 个字节将存储在 SV 中(并且 length() 将报告值为 8)。此条目类似于 T_OPAQUE。
原则上,unpack() 命令可以用来将字节转换回数字(如果底层类型已知为数字)。
此条目可以用来存储 C 结构体(要复制的字节数使用 C sizeof
函数计算),并且可以用作 T_PTRREF 的替代方案,而无需担心内存泄漏(因为 Perl 将清理 SV)。
这可以用来在 SV 的字符串部分存储来自非指针类型的数据。它类似于 T_OPAQUEPTR,但类型映射直接检索指针,而不是假设它正在被提供。例如,如果一个整数使用 T_OPAQUE 而不是 T_IV 导入到 Perl 中,那么表示该整数的底层字节将存储在 SV 中,但实际的整数值将不可用。即数据对 Perl 是不透明的。
如果已知字节流的底层类型,可以使用 unpack
函数检索数据。
T_OPAQUE 支持简单类型的输入和输出。如果指针可以接受,可以使用 T_OPAQUEPTR 将这些字节传回 C。
xsubpp 支持一种特殊的语法,用于将打包的 C 数组返回到 perl。如果 XS 返回类型被指定为
array(type, nelem)
xsubpp 将从 RETVAL 复制 nelem * sizeof(type)
字节的内容到一个 SV,并将其推送到堆栈。这只有在要返回的项目数量在编译时已知,并且你不介意在你的 SV 中有一个字节字符串时才真正有用。使用 T_ARRAY 将可变数量的参数推送到返回堆栈(它们不会被打包为单个字符串)。
这类似于使用 T_OPAQUEPTR,但可以用来处理多个元素。
调用用户提供的转换函数。对于 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);
除了实际的 C 函数必须使用 dTHX
获取线程上下文之外,你还可以定义相同名称的宏,并避免开销。此外,请记住可能需要释放 XS_unpack_foo_tPtr
分配的内存。
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) 个指针,以方便迭代。
或者,从一开始就使用自定义类型映射来满足您的目的可能更可取。
尚未实现
尚未实现
它用于将 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' 标签都将从名称中删除,以确定子类型。
这用于使用 FILE *
结构将 Perl 文件句柄传递到 C 和从 C 传递到 Perl。
这用于使用 PerlIO *
结构将 Perl 文件句柄传递到 C 和从 C 传递到 Perl。文件句柄可用于读写。这对应于 +<
模式,另请参见 T_IN 和 T_OUT。
有关 Perl IO 抽象层的更多信息,请参见 perliol。Perl 必须使用 -Duseperlio
构建。
没有检查来断言从 Perl 传递到 C 的文件句柄是用正确的 open()
模式创建的。
提示:perlxstut 教程很好地介绍了 T_INOUT、T_IN 和 T_OUT XS 类型。
与 T_INOUT 相同,但从 C 返回到 Perl 的文件句柄只能用于读取(模式 <
)。
与 T_INOUT 相同,但从 C 返回到 Perl 的文件句柄设置为使用打开模式 +>
。