gpt4 book ai didi

python - 模块加载在CPython中如何工作?

转载 作者:行者123 更新时间:2023-12-03 23:35:29 32 4
gpt4 key购买 nike

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

我发现源代码本身不堪重负。我可以看到,在支持它的系统上使用了可信赖的ol'dlopen()和友人,但是对总体情况没有任何了解,这将需要很长时间才能从源代码中弄清楚。

关于这个话题可以写很多,但据我所知,几乎没有任何东西-大量描述Python语言本身的网页使搜索变得很困难。一个很好的答案将提供一个合理的简要概述,并提供我可以学到更多资源的参考。

我最关心的是在类似Unix的系统上这是如何工作的,仅因为这就是我所知道的,但是我对过程在其他地方是否类似感兴趣。

更具体地说(可能冒太多的风险),CPython如何使用模块方法表和初始化函数来“理解”动态加载的C?

最佳答案

TLDR短版本以粗体显示。

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

Python通过动态加载导入大多数用C编写的扩展。动态加载是一个深奥的主题,虽然没有充分的文档记载,但这是绝对的前提。在解释Py​​thon如何使用它之前,我必须简要解释一下它是什么以及Python为什么使用它。

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

链接使我们能够跨编译的代码单元进行函数调用。当在运行时决定要链接的内容时,动态加载解决了链接代码的问题。也就是说,它允许正在运行的程序与链接器交互,并告诉链接器它要链接的内容。为了让Python解释器使用C代码导入模块,这就是需要的。编写在运行时做出此决定的代码非常少见,并且大多数程序员会对它的实现感到惊讶。简而言之,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将具有符号“ say_hi1”。它使用dlopen()获取dyload1.so符号的句柄,使用dlsym()获取符号的地址,并假定它是一个不带参数且不返回任何内容的函数,然后调用它。它无法确定“ say_hi1”是什么-事先达成的协议足以阻止我们进行段隔离。

我上面显示的是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)


这些功能包含用于不同操作系统的不同动态加载机制。在Mac OS 10.2和大多数Unix(类似)系统上动态加载的机制是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进行自身初始化。此调用将模块添加到解释器保存的“字典” /哈希表中,该解释器将模块名称映射到模块数据。它还将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


我们的方法表恰好称为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函数均具有相同的签名。

通过使用一组有限的C函数签名,Python规避了动态加载中的许多困难。大多数C函数特别使用一个签名。可以通过转换为PyCFunction来“插入”带有附加参数的C函数的指针。 (请参见 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.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结尾。

关于python - 模块加载在CPython中如何工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/25678174/

32 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com