TLDR短版加粗。在
对Python源代码的引用基于2.7.6版。在
Python通过动态加载导入用C编写的大多数扩展。动态加载是一个深奥的主题,虽然没有很好的文档记录,但它绝对是一个先决条件。在解释Python为什么要使用em之前,请简要解释一下Python是如何使用的。在
过去,对Python的C扩展是静态链接到Python解释器本身的。这就要求Python用户每次想使用用C编写的新模块时都要重新编译解释器。正如您所想象的,而且正如{a1},随着社区的发展,这变得不切实际了。今天,大多数Python用户从未编译过一次解释器。我们只需“pip install module”然后“import module”,即使该模块包含已编译的C代码。在
链接使我们能够跨编译的代码单元进行函数调用。动态加载解决了在运行时决定链接什么时链接代码的问题。也就是说,它允许正在运行的程序与链接器接口,并告诉链接器它要链接什么。对于Python解释器来说,用C代码导入模块,这正是我们所需要的。编写在运行时做出这种决定的代码是非常罕见的,大多数程序员都会惊讶于这是可能的。简单地说,一个C函数有一个地址,它期望您将某些数据放在某些位置,并且它承诺在返回时将某些数据放在某些位置。如果你知道秘密握手,你可以称之为。在
动态加载的挑战是程序员有责任获得正确的握手,而且没有安全检查。至少,他们不是为我们准备的。通常,如果我们试图用不正确的签名调用函数名,就会得到编译或链接器错误。使用动态加载,我们在运行时按名称(一个“符号”)向链接器请求函数。链接器可以告诉我们是否找到了该名称,但它不能告诉我们如何调用该函数。它只给我们一个地址-一个空指针。我们可以尝试强制转换为某种类型的函数指针,但这完全取决于程序员是否正确转换。如果在转换中函数签名错误,编译器或链接器警告我们就太迟了。在程序失去控制并以不恰当的方式访问内存之后,我们可能会得到一个segfault。使用动态加载的程序必须依赖预先安排好的约定和运行时收集的信息来进行正确的函数调用。在我们处理Python解释器之前,这里有一个小例子。在
文件1:main.c/* gcc-4.8 -o main main -ldl */
#include /* key include, also in Python/dynload_shlib.c */
/* used for cast to pointer to function that takes no args and returns nothing */
typedef void (say_hi_type)(void);
int main(void) {
/* get a handle to the shared library dyload1.so */
void* handle1 = dlopen("./dyload1.so", RTLD_LAZY);
/* acquire function ptr through string with name, cast to function ptr */
say_hi_type* say_hi1_ptr = (say_hi_type*)dlsym(handle1, "say_hi1");
/* dereference pointer and call function */
(*say_hi1_ptr)();
return 0;
}
/* error checking normally follows both dlopen() and dlsym() */
文件2:dyload1.c
^{pr2}$
这些文件是单独编译和链接的,但是main.c知道在运行时查找./dyload1.so。main中的代码假定dyload1.so将有一个符号“say_hi1”。它使用dlopen()获取dyload1的句柄,使用dlsym()获取符号的地址,假设它是一个不带参数、不返回任何值的函数,然后调用它。它无法确切地知道“说好”是什么是一个事先达成的协议,是我们避免分裂的唯一原因。在
上面我展示的是dlopen()函数族。Python部署在许多平台上,并非所有平台都提供dlopen(),但大多数平台都具有类似的动态加载机制。Python通过将多个操作系统的动态加载机制封装在一个公共接口中,实现了可移植的动态加载。
Python/importdl.c中的这条注释总结了该策略。在/* ./configure sets HAVE_DYNAMIC_LOADING if dynamic loading of modules is
supported on this platform. configure will then compile and link in one
of the dynload_*.c files, as appropriate. We will call a function in
those modules to get a function pointer to the module's init function.
*/
如前所述,在Python 2.7.6中,我们有以下dynload*.c文件:Python/dynload_aix.c Python/dynload_beos.c Python/dynload_hpux.c
Python/dynload_os2.c Python/dynload_stub.c Python/dynload_atheos.c
Python/dynload_dl.c Python/dynload_next.c Python/dynload_shlib.c
Python/dynload_win.c
它们各自用以下签名定义函数:dl_funcptr _PyImport_GetDynLoadFunc(const char *fqname, const char *shortname,
const char *pathname, FILE *fp)
这些函数包含针对不同操作系统的不同动态加载机制。机械师在10.2以上的macos和大多数Unix(-like)系统上动态加载的anism是dlopen(),它在Python/dynload_shlib.c中调用
略过dynload_win.c,Windows的分析函数是LoadLibraryEx()。它的用途看起来非常相似。在
在Python/dynload_shlib.c的底部可以看到对dlopen()和dlsym()的实际调用。在handle = dlopen(pathname, dlopenflags);
/* error handling */
p = (dl_funcptr) dlsym(handle, funcname);
return p;
在这之前,Python用它要查找的函数名组成字符串。模块名在shortname变量中。在PyOS_snprintf(funcname, sizeof(funcname),
LEAD_UNDERSCORE "init%.200s", shortname);
Python只希望有一个名为init{modulename}的函数,并向链接器请求它。从这里开始,Python依赖于一组小的约定,使C代码的动态加载成为可能和可靠的。
让我们看看C扩展必须做些什么来完成使上面对dlsym()的调用起作用的契约。对于编译后的C Python模块,允许Python访问编译后的C代码的第一个约定是init{shared_library_filename}()函数。对于a module named spam编译为共享库,名为“垃圾邮件.so,我们可以提供initspam()函数:PyMODINIT_FUNC
initspam(void)
{
PyObject *m;
m = Py_InitModule("spam", SpamMethods);
if (m == NULL)
return;
}
如果init函数的名称与文件名不匹配,Python解释器将无法知道如何找到它。例如,重命名垃圾邮件.so到不是垃圾邮件尝试导入会产生以下结果。在>>> import spam
ImportError: No module named spam
>>> import notspam
ImportError: dynamic module does not define init function (initnotspam)
如果违反了命名约定,则无法判断共享库是否包含初始化函数。在
第二个键约定是,一旦被调用,init函数负责通过调用Py_InitModule来初始化自己。这个调用将模块添加到解释器保存的“dictionary”/哈希表中,解释器将模块名映射到模块数据。它还将C函数注册到方法表中。调用Py_InitModule后,模块可以通过其他方式初始化自己,例如添加对象。(例如:the SpamError object in the Python C API tutorial)。(Py_InitModule实际上是一个宏,它创建了真正的init调用,但是包含了一些信息,比如我们编译的C扩展使用的Python版本)
如果init函数有正确的名称,但没有调用Py_InitModule(),则得到:SystemError: dynamic module not initialized properly
我们的methods表恰好被称为SpamMethods,看起来像这样。在static PyMethodDef SpamMethods[] = {
{"system", spam_system, METH_VARARGS,
"Execute a shell command."},
{NULL, NULL, 0, NULL}
};
方法表本身及其所包含的函数签名契约是Python理解动态加载的C所必需的第三个也是最后一个键约定。方法表是一个struct PyMethodDef数组,最后一个sentinel条目。PyMethodDef在Include/methodobject.h中定义如下。在
^{12}$
这里的关键部分是第二个成员是一个PyCFunction。我们传入了一个函数的地址,那么什么是PyCFunction?它是一个typedef,也在Include/methodobject.h中typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);
PyCFunction是指向函数的指针的typedef,该函数返回指向PyObject的指针,并接受指向PyObjects的两个指针作为参数。作为约定三的引理,注册到方法表中的C函数都具有相同的签名。
Python通过使用一组有限的C函数签名来规避动态加载的许多困难。大多数C函数都使用一个签名。指向带有附加参数的C函数的指针可以通过强制转换为PyCFunction而“潜入”。(请参阅Python C API tutorial中的keywdarg_parrot示例)即使是备份Python函数但在Python中不带参数的C函数也会在C中使用两个参数(如下所示)。所有函数也被期望返回一些东西(可能只是None对象)。在Python中接受多个位置参数的函数必须将这些参数从C中的单个对象中解包出来
这就是如何获取和存储与动态加载的C函数接口的数据。最后,下面是一个如何使用这些数据的示例。在
这里的背景我们在逐个指令地计算Python的“操作码”,我们找到了一个函数调用操作码。(见https://docs.python.org/2/library/dis.html。我们已经确定Python函数对象是由一个C函数支持的。在下面的代码中,我们检查Python中的函数是否不带参数(在Python中),如果是,则调用它(在C中使用两个参数)。在
Python/ceval.cif (flags & (METH_NOARGS | METH_O)) {
PyCFunction meth = PyCFunction_GET_FUNCTION(func);
PyObject *self = PyCFunction_GET_SELF(func);
if (flags & METH_NOARGS && na == 0) {
C_TRACE(x, (*meth)(self,NULL));
}
当然,它需要C语言中的参数-正好是两个。因为在Python中,所有东西都是一个对象,所以它有一个自参数。在底部您可以看到,meth被分配了一个函数指针,然后该指针被取消引用并被调用。返回值以x结尾