SWIG4 --- 13 Typemaps
13.1 Introduction
你阅读这一章的原因可能有两个;您要么想要定制SWIG的行为,要么无意中听到有人在嘀咕一些关于“typemaps”的难以理解的废话,然后您问自己“typemaps,那些是什么?”也就是说,让我们从一个简短的免责声明开始,即“typemaps”是一种高级定制特性,可以直接访问SWIG的低级代码生成器。不仅如此,它们还是SWIG c++类型系统的重要组成部分(这本身就不是一个简单的主题)。类型映射通常不是使用SWIG的必要部分。因此,如果您在阅读本章时对SWIG在默认情况下已经做了什么只有一个模糊的概念,那么您可能需要重新阅读前面的章节。
13.1.1 Type conversion
包装器代码生成中最重要的问题之一是编程语言之间数据类型的转换或编组。特别是,对于每个C/ c++声明,SWIG必须以某种方式生成允许在语言之间来回传递值的包装器代码。由于每种编程语言都以不同的方式表示数据,所以这不是简单地用C链接器将代码链接到一起的问题。相反,SWIG必须了解如何用每种语言表示数据以及如何操作数据。
为了说明这一点,假设你有一个像这样的简单C函数:
int factorial(int n);
要从Python访问这个函数,需要使用一对Python API函数来转换整数值。例如:
long PyInt_AsLong(PyObject *obj); /* Python --> C */ PyObject *PyInt_FromLong(long x); /* C --> Python */
第一个函数用于将输入参数从Python整数对象转换为C long对象。第二个函数用于将一个值从C转换回Python整数对象。
PyObject *wrap_factorial(PyObject *self, PyObject *args) { int arg1; int result; PyObject *obj1; PyObject *resultobj; if (!PyArg_ParseTuple("O:factorial", &obj1)) return NULL; arg1 = PyInt_AsLong(obj1); result = factorial(arg1); resultobj = PyInt_FromLong(result); return resultobj; }
SWIG支持的每一种目标语言都具有以类似方式工作的函数。例如,在Perl中,使用了以下函数:
IV SvIV(SV *sv); /* Perl --> C */ void sv_setiv(SV *sv, IV val); /* C --> Perl */
In Tcl:
int Tcl_GetLongFromObj(Tcl_Interp *interp, Tcl_Obj *obj, long *value); Tcl_Obj *Tcl_NewIntObj(long value);
精确的细节并不是那么重要。重要的是,所有底层类型转换都是由实用函数集合和像这样的一小段C代码处理的——您只需要阅读您喜欢的语言的扩展文档就可以知道它是如何工作的(留给读者的练习)。
13.1.2 Typemaps
由于类型处理对于包装器代码生成非常重要,因此SWIG允许用户完全定义(或重新定义)类型处理。为此,需要使用一个特殊的%typemap指令。例如:
/* Convert from Python --> C */ %typemap(in) int { $1 = PyInt_AsLong($input); } /* Convert from C --> Python */ %typemap(out) int { $result = PyInt_FromLong($1); }
乍一看,这段代码看起来有点混乱。然而,真的没有太多的东西。第一个typemap(“in”typemap)用于将一个值从目标语言转换为c语言。第二个typemap(“out”typemap)用于在另一个方向进行转换。每个typemap的内容是一小段代码,直接插入SWIG生成的包装器函数中。代码通常是C或c++代码,这些代码将生成为C/ c++包装器函数。注意,并非总是如此,因为一些目标语言模块允许在类型映射中生成目标语言特定文件的目标语言代码。在这段代码中,扩展了许多以$为前缀的特殊变量。这些实际上只是在创建包装器函数过程中生成的C/ c++变量的占位符。在本例中,$input指的是需要转换为C/ c++的输入对象,$result指的是包装器函数返回的对象。$1指的是一个C/ c++变量,它具有与typemap声明中指定的类型相同的类型(在本例中为int)。
一个简短的例子可能会使这一点更清楚。如果你像这样包装一个函数:
int gcd(int x, int y);
包装器函数大致如下所示:
PyObject *wrap_gcd(PyObject *self, PyObject *args) { int arg1; int arg2; int result; PyObject *obj1; PyObject *obj2; PyObject *resultobj; if (!PyArg_ParseTuple("OO:gcd", &obj1, &obj2)) return NULL; /* "in" typemap, argument 1 */ { arg1 = PyInt_AsLong(obj1); } /* "in" typemap, argument 2 */ { arg2 = PyInt_AsLong(obj2); } result = gcd(arg1, arg2); /* "out" typemap, return value */ { resultobj = PyInt_FromLong(result); } return resultobj; }
在这段代码中,您可以看到typemap代码是如何插入到函数中的。您还可以看到如何将特殊的$变量展开以匹配包装器函数中的某些变量名。这实际上就是typemaps背后的全部思想——它们只是让您将任意代码插入生成的包装器函数的不同部分。因为可以插入任意代码,所以可以完全改变转换值的方式。
13.1.3 Pattern matching
顾名思义,typemap的目的是将C数据类型“映射”到目标语言中的类型。一旦为C数据类型定义了typemap,它将应用于输入文件中该类型未来出现的所有情况。例如:
/* Convert from Perl --> C */ %typemap(in) int { $1 = SvIV($input); } ... int factorial(int n); int gcd(int x, int y); int count(char *s, char *t, int max);
类型映射到C数据类型的匹配不仅仅是简单的文本匹配。事实上,类型映射完全内置在底层类型系统中。因此,typemaps不受typedef、名称空间和其他可能隐藏底层类型的声明的影响。例如,你可以有这样的代码:
/* Convert from Ruby--> C */ %typemap(in) int { $1 = NUM2INT($input); } ... typedef int Integer; namespace foo { typedef Integer Number; }; int foo(int x); int bar(Integer y); int spam(foo::Number a, foo::Number b);
在这种情况下,typemap仍然应用于适当的参数,即使typename并不总是匹配文本“int”。这种跟踪类型的能力是SWIG的一个关键部分——事实上,所有目标语言模块都只是为基本类型定义了一组类型映射。然而,从来没有必要为typedef引入的类型名编写新的类型映射。
除了跟踪类型名称之外,类型映射还可以专门化以匹配特定的参数名称。例如,你可以这样写一个typemap:
%typemap(in) double nonnegative { $1 = PyFloat_AsDouble($input); if ($1 < 0) { PyErr_SetString(PyExc_ValueError, "argument must be nonnegative."); SWIG_fail; } } ... double sin(double x); double cos(double x); double sqrt(double nonnegative); typedef double Real; double log(Real nonnegative); ...
对于某些任务,如输入参数转换,可以为连续参数序列定义类型映射。例如:
%typemap(in) (char *str, int len) { $1 = PyString_AsString($input); /* char *str */ $2 = PyString_Size($input); /* int len */ } ... int count(char *str, int len, char c);
在本例中,单个输入对象被扩展为一对C参数。这个示例还提示了不同寻常的变量命名方案,包括$1、$2等等。
13.4.3 Special variables
在所有类型映射中,将展开以下特殊变量。这绝不是一个完整的列表,因为一些目标语言有额外的特殊变量,这些变量在语言特定的章节中有文档说明。
Variable | Meaning |
---|---|
$n | A C local variable corresponding to type n in the typemap pattern. |
$argnum | Argument number. Only available in typemaps related to argument conversion |
$n_name | Argument name |
$n_type | Real C datatype of type n. |
$n_ltype | ltype of type n |
$n_mangle | Mangled form of type n. For example _p_Foo |
$n_descriptor | Type descriptor structure for type n. For example SWIGTYPE_p_Foo. This is primarily used when interacting with the run-time type checker (described later). |
$*n_type | Real C datatype of type n with one pointer removed. |
$*n_ltype | ltype of type n with one pointer removed. |
$*n_mangle | Mangled form of type n with one pointer removed. |
$*n_descriptor | Type descriptor structure for type n with one pointer removed. |
$&n_type | Real C datatype of type n with one pointer added. |
$&n_ltype | ltype of type n with one pointer added. |
$&n_mangle | Mangled form of type n with one pointer added. |
$&n_descriptor | Type descriptor structure for type n with one pointer added. |
$n_basetype | Base typename with all pointers and qualifiers stripped. |
13.12 The run-time type checker
大多数脚本语言在运行时都需要类型信息。这种类型信息可以包括如何构造类型、如何垃圾收集类型以及类型之间的继承关系。如果语言接口不提供自己的类型信息存储,则生成的SWIG代码需要提供它。
Requirements for the type system:
- 存储继承和类型等价的信息,并能正确地重新创建类型指针。
- 在模块之间共享类型信息。
- 模块可以按任何顺序加载,而不考虑实际的类型依赖性。
- 避免使用动态分配的内存,以及一般的库/系统调用。
- 提供一个合理的快速实现,尽量减少所有语言模块的查找时间。
- 自定义的、特定于语言的信息可以被附加到类型上。
- 模块可以从类型系统中卸载。
13.12.1 Implementation
许多(但不是所有)SWIG支持的目标语言都使用运行时类型检查器。运行时类型检查器特性不是必需的,因此不用于静态类型语言,如Java和c#。基于脚本和模式的语言依赖于它,它构成了SWIG对这些语言的操作的关键部分。
当指针、数组和对象被SWIG包装时,它们通常被转换为类型化的指针对象。例如,Foo *的一个实例可能是一个编码如下的字符串:
_108e688_p_Foo
在基本级别上,类型检查器只是为扩展模块恢复一些类型安全。然而,类型检查器也负责确保包装的c++类得到正确的处理——尤其是在使用继承时。当扩展模块使用多重继承时,这一点尤其重要。例如:
class Foo { public: int x; }; class Bar { public: int y; }; class FooBar : public Foo, public Bar { public: int z; };
当类FooBar在内存中组织时,它包含类Foo和Bar的内容以及它自己的数据成员。例如:
FooBar --> | -----------| <-- Foo | int x | |------------| <-- Bar | int y | |------------| | int z | |------------|
由于基类数据堆叠在一起的方式,将Foobar *转换为任意一个基类可能会改变指针的实际值。这意味着通常使用简单的整数或空*表示指针是不安全的——需要使用类型标记来实现对指针值的正确处理(并在需要时进行调整)。
在为每种语言生成的包装器代码中,指针是通过使用特殊类型描述符和转换函数来处理的。例如,如果你查看Python的包装器代码,你会看到类似如下的代码(为了简洁而简化):
if (!SWIG_IsOK(SWIG_ConvertPtr(obj0, (void **) &arg1, SWIGTYPE_p_Foo, 0))) { SWIG_exception_fail(SWIG_TypeError, "in method 'GrabVal', expecting type Foo"); }
在这段代码中,SWIGTYPE_p_Foo是描述Foo *的类型描述符。类型描述符实际上是指向一个结构的指针,该结构包含关于目标语言中使用的类型名的信息,一个等价类型名的列表(通过typedef或继承),以及指针值处理信息(如果适用)。SWIG_ConvertPtr()函数只是一个实用函数,它接受目标语言中的一个指针对象和一个类型描述符对象,并使用该信息生成一个c++指针。SWIG_IsOK宏检查返回值是否有错误,可以调用SWIG_exception_fail在目标语言中引发异常。但是,转换函数的确切名称和调用约定取决于目标语言(有关详细信息,请参阅语言相关章节)。
实际的类型代码在swigrun.swg,并且插入到生成的swig包装器文件的顶部附近。“一种类型X可以转换为一个类型Y”意味着给定类型X,它可以转换成一个类型Y .换句话说,X是Y的派生类或X是Y的类型结构来存储类型信息是这样的:
/* Structure to store information on one type */ typedef struct swig_type_info { const char *name; /* mangled name of this type */ const char *str; /* human readable name for this type */ swig_dycast_func dcast; /* dynamic cast function down a hierarchy */ struct swig_cast_info *cast; /* Linked list of types that can cast into this type */ void *clientdata; /* Language specific type data */ } swig_type_info; /* Structure to store a type and conversion function used for casting */ typedef struct swig_cast_info { swig_type_info *type; /* pointer to type that is equivalent to this type */ swig_converter_func converter; /* function to cast the void pointers */ struct swig_cast_info *next; /* pointer to next cast in linked list */ struct swig_cast_info *prev; /* pointer to the previous cast */ } swig_cast_info;
每个swig_type_info存储一个与之等价的类型链表。这个双向链表中的每个条目都存储一个指向另一个swig_type_info结构的指针,以及一个指向转换函数的指针。这个转换函数用于解决上面FooBar类的问题,正确地返回一个指向我们想要的类型的指针。
我们需要解决的基本问题是验证和构建传递给函数的参数。所以回到上面的SWIG_ConvertPtr()函数示例,我们期望一个Foo *,需要检查obj0是否实际上是一个Foo *。在前面,SWIGTYPE_p_Foo只是一个指向swig_type_info结构的指针,该结构描述Foo *。因此,我们循环了连接到SWIGTYPE_p_Foo的swig_cast_info结构的链表。如果我们看到obj0的类型在链表中,我们通过关联的转换函数传递该对象,然后返回一个正数。如果我们到达链表的末尾没有匹配,那么obj0就不能转换为Foo *,并产生一个错误。
另一个需要解决的问题是在多个模块之间共享类型信息。更明确地说,我们需要为每个类型提供一个swig_type_info。如果两个模块都使用该类型,则加载的第二个模块必须查找并使用已加载模块的swig_type_info结构。因为不使用动态内存,而且强制转换信息的循环依赖关系,所以加载类型信息有些棘手,这里没有解释。完整的描述在Lib/swiginit中。SWG文件(靠近任何生成文件的顶部)。
每个模块都有一个swig_module_info结构,看起来像这样:
/* Structure used to store module information * Each module generates one structure like this, and the runtime collects * all of these structures and stores them in a circularly linked list.*/ typedef struct swig_module_info { swig_type_info **types; /* Array of pointers to swig_type_info structs in this module */ int size; /* Number of types in this module */ struct swig_module_info *next; /* Pointer to next element in circularly linked list */ swig_type_info **type_initial; /* Array of initially generated type structures */ swig_cast_info **cast_initial; /* Array of initially generated casting structures */ void *clientdata; /* Language specific module data */ } swig_module_info;
每个模块存储一个指向swig_type_info结构的指针数组,以及该模块中类型的数量。因此,当加载第二个模块时,它找到第一个模块的swig_module_info结构,并搜索类型数组。如果它自己的任何类型在第一个模块中并且已经被加载,它将使用那些swig_type_info结构而不是创建新的结构。这些swig_module_info结构被链接在一个循环链表中。