模块加载如何在Python中工作?

时间:2014-09-05 03:21:44

标签: python python-import cpython dynamic-loading python-internals

模块加载如何在CPython下工作?特别是,用C编写的扩展的动态加载如何工作?我在哪里可以了解到这一点?

我发现源代码本身相当压倒性。我可以看到可靠的ol' dlopen()和朋友用在支持它的系统上但没有任何大局观,从源代码中解决这个问题需要很长时间。

可以在这个主题上写出大量的内容,但据我所知,几乎没有任何内容 - 描述Python语言本身的大量网页使搜索变得困难。一个很好的答案将提供一个相当简短的概述和参考资源,我可以了解更多。

我主要关心的是如何在类Unix系统上运行,因为这是我所知道的,但我感兴趣的是,如果这个过程与其他地方类似。

为了更具体(但风险也假设太多),CPython如何使用模块方法表和初始化函数来理解"动态加载C?

1 个答案:

答案 0 :(得分:17)

TLDR短版加粗。

对Python源代码的引用基于2.7.6版。

Python通过动态加载导入大部分用C编写的扩展。动态加载是一个深奥的主题,没有很好的文档记录,但它是绝对的先决条件。在解释 Python如何使用它之前,我必须简要解释一下它是什么为什么 Python使用它。

历史上,Python的C扩展与Python解释器本身静态链接。这需要Python用户每次想要使用用C编写的新模块时重新编译解释器。正如您可以想象的那样,随着社区的发展,这变得不可行。Guido van Rossum describes今天,大多数Python用户从未编译过一次解释器。我们只是简单地安装模块"然后"导入模块"即使该模块包含已编译的C代码。

链接是允许我们跨编译的代码单元进行函数调用的原因。动态加载解决了在运行时决定链接内容时链接代码的问题。也就是说,它允许正在运行的程序与链接器进行交互并告诉链接器它想要链接的内容。 对于使用C代码导入模块的Python解释器,这就是所要求的。编写在运行时做出此决定的代码非常罕见,并且大多数程序员会对它感到惊讶。可能。简单地说,C函数有一个地址,它希望你将某些数据放在某些地方,并且它承诺在返回时将某些数据放在某些地方。如果你知道秘密握手,你可以打电话给它。

动态加载的挑战在于程序员有责任正确握手并且没有安全检查。至少,他们没有为我们提供。通常,如果我们尝试使用不正确的签名调用函数名称,则会出现编译或链接器错误。 通过动态加载,我们在运行时通过名称("符号")向链接器询问函数。链接器可以告诉我们是否找到了该名称,但它无法告诉我们如何调用该函数。它只是给我们一个地址 - 一个无效指针。我们可以尝试转换为某种类型的函数指针,但完全取决于程序员来使得转换正确。如果我们在演员表中得到错误的函数签名,那么编译器或链接器就太迟了警告我们。在程序失控后最终会错误地访问内存,我们可能会遇到段错误。 使用动态加载的程序必须依赖于预先安排的约定和在运行时收集的信息才能进行正确的函数调用。在我们处理Python解释器之前,这是一个小例子。

文件1:main.c

/* gcc-4.8 -o main main -ldl */
#include <dlfcn.h> /* 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

/* gcc-4.8 -o dyload1.so dyload1.c -shared -fpic */
/* compile as C, C++ does name mangling -- changes function names */
#include <stdio.h>

void say_hi1() {
    puts("dy1: hi");
}

这些文件是单独编译和链接的,但main.c知道在运行时寻找./dyload1.so。 main中的代码假定dyload1.so将有一个符号&#34; say_hi1&#34;。它使用dlopen()获取dyload1.so符号的句柄,使用dlsym()获取符号的地址,假设它是一个不带参数且不返回任何内容的函数,并调用它。它无法确定是什么&#34; say_hi1&#34;是 - 先前的协议是让我们免于分裂的一切。

我上面展示的是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和大多数Unix(类似)系统的Mac OS上动态加载的机制是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}()函数。对于编译为名为“spam.so”的共享库的a module named spam “,我们可能会提供这个initspam()函数:

PyMODINIT_FUNC
initspam(void)
{
    PyObject *m;
    m = Py_InitModule("spam", SpamMethods);
    if (m == NULL)
        return;
}

如果init函数的名称与文件名不匹配,则Python解释器无法知道如何找到它。例如,将spam.so重命名为notspam.so并尝试导入将提供以下内容。

>>> import spam
ImportError: No module named spam
>>> import notspam
ImportError: dynamic module does not define init function (initnotspam)

如果违反了命名约定,则根本不知道共享库是否包含初始化函数。

第二个关键约定是,一旦调用,init函数负责通过调用Py_InitModule来初始化自己。这个调用将模块添加到&#34;字典&#34; / hash表保存由将模块名称映射到模块数据的解释器。它还在方法表中注册C函数。在调用Py_InitModule之后,模块可以通过其他方式初始化自己,例如添加对象。 (例如:the SpamError object in the Python C API tutorial)。 (Py_InitModule实际上是一个宏,它创建了真正的初始化调用,但有一些信息被烘焙,就像我们编译的C扩展使用的Python版本一样。)

如果init函数具有正确的名称但没有调用Py_InitModule(),我们得到:

SystemError: dynamic module not initialized properly

我们的方法表恰好被称为SpamMethods,看起来像这样。

static PyMethodDef SpamMethods[] = {
    {"system", spam_system, METH_VARARGS,
     "Execute a shell command."},
    {NULL, NULL, 0, NULL}
};

方法表本身和它所需的函数签名契约是Python理解动态加载C所必需的第三个也是最后一个键约定。方法表是一个struct PyMethodDef的数组,带有一个最后的哨兵入境。 PyMethodDef在Include / methodobject.h中定义如下。

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction  ml_meth;   /* The C function that implements it */
    int      ml_flags;  /* Combination of METH_xxx flags, which mostly
                   describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};

这里的关键部分是第二个成员是PyCFunction。我们传入了函数的地址,那么什么是PyCFunction?它也是一个typedef,也在Include / methodobject.h中

typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);

PyCFunction是一个指向函数的指针的typedef,该函数返回一个指向PyObject的指针,并为参数提供两个指向PyObjects的指针。 作为约定三的引理,在方法表中注册的C函数都具有相同的签名。

Python通过使用一组有限的C函数签名来规避动态加载的大部分困难。特别是一个签名用于大多数C函数。指向其他参数的C函数的指针可能会被&#34;偷偷摸摸进入&#34;通过强制转换为PyCFunction。 (参见Python C API tutorial中的keywdarg_parrot示例。)即使是在Python中没有参数的Python函数的C函数,也会在C中使用两个参数(如下所示)。期望所有函数都返回一些东西(可能只是None对象)。在Python中使用多个位置参数的函数必须从C中的单个对象解包这些参数。

如何获取和存储与动态加载的C函数接口的数据。最后,这是一个如何使用该数据的例子。

这里的背景是我们正在评估Python&#34;操作码&#34;,逐条指令,我们已经点击了函数调用操作码。 (参见https://docs.python.org/2/library/dis.html。值得一试。)我们已经确定Python函数对象是由C函数支持的。在下面的代码中,我们检查Python中的函数是否不带参数(在Python中),如果是,则调用它(在C中有两个参数)。

的Python / ceval.c。

if (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。

结尾