Как работает загрузка модуля в CPython?

Как работает загрузка модуля в CPython под капотом? Особенно, как работает динамическая загрузка расширений, написанных на C? Где я могу узнать об этом?

Я нахожу сам исходный код довольно подавляющим. Я вижу этого верного старика!--0--> и друзья используются в системах, которые поддерживают его, но без какого-либо чувства большей картины потребуется много времени, чтобы понять это из исходного кода.

огромное количество может быть написано на этой но, насколько я могу судить, почти ничего не было - обилие веб-страниц, описывающих сам язык Python, затрудняет поиск. Отличный ответ предоставит достаточно краткий обзор и ссылки на ресурсы, где я могу узнать больше.

меня в основном интересует, как это работает в Unix-подобных системах, просто потому, что это то, что я знаю, но меня интересует, похож ли процесс в другом месте.

чтобы быть более конкретным (но и риск предполагая слишком много), как CPython использует таблицу методов модуля и функцию инициализации, чтобы "иметь смысл" динамически загруженного C?

1 ответов


короткая версия TLDR выделена жирным шрифтом.

ссылки на исходный код Python основаны на версии 2.7.6.

Python импортирует большинство расширений, написанных на C, через динамическую загрузку. динамическая загрузка-это эзотерическая тема, которая плохо документирована, но является абсолютным предварительным условием. Прежде чем объяснить как Python использует его, я должен кратко объяснить что это и почему Python использует он.

исторически расширения C для Python были статически связаны с самим интерпретатором Python. Это требует пользователей Python для компиляции интерпретатор каждый раз, когда они хотели использовать новый модуль, написанный на C. Как вы можете себе представить, и как Гвидо ван Россум описывает, это стало непрактичным, поскольку сообщество выросло. Сегодня большинство пользователей Python никогда не компилируют интерпретатор один раз. Мы просто "pip install module", а затем "import module", даже если этот модуль содержит скомпилированный код C.

связывание-это то, что позволяет нам совершать вызовы функций через скомпилированные единицы кода. Динамическая загрузка решает проблему связывания кода, когда решение, что связь во время выполнения. то есть он позволяет запущенной программе взаимодействовать с компоновщиком и сообщать компоновщику, с чем он хочет связать. для интерпретатора Python для импорта модулей с кодом C это то, что требуется. написание кода, который делает это решение во время выполнения довольно необычно, и большинство программистов были бы удивлены, что это возможно. Проще говоря, функция C имеет адрес, она ожидает, что вы поместите определенные данные в определенные места, и она обещает поместить определенные данные в определенные места по возвращении. Если вы знаете секрет рукопожатия, вы можете назвать его.

проблема с динамической нагрузкой заключается в том, что программист обязан правильно пожать руку, и нет никаких проверок безопасности. По крайней мере, они не обеспечил нас. Обычно, если мы пытаемся вызвать имя функции с неправильной сигнатурой, мы получаем ошибку компиляции или компоновщика. при динамической загрузке мы запрашиваем у компоновщика функцию по имени ("символ") во время выполнения. Компоновщик может сказать нам, было ли найдено это имя, но он не может сказать нам, как вызвать эту функцию. Он просто дает нам адрес-указатель пустоты. Мы можем попытаться привести к указателю функции, но только программист должен получить правильное приведение. если мы получим подпись функции неправильная в нашем приведении, слишком поздно для компилятора или компоновщика, чтобы предупредить нас. Скорее всего, мы получим segfault после того, как программа выйдет из-под контроля и закончится неправильным доступом к памяти. программы, использующие динамическую загрузку, должны полагаться на предварительно организованные соглашения и информацию, собранную во время выполнения, чтобы сделать правильные вызовы функций. вот небольшой пример, прежде чем мы займемся интерпретатором 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");
}

эти файлы компилируются и связаны отдельно, но основные.c знает, что нужно искать ./dyload1.так что во время выполнения. Код в main предполагает, что dyload1.так будет иметь символ "say_hi1". Он получает ручку dyload1.so-символы с dlopen(), получает адрес символа с помощью dlsym (), предполагает, что это функция, которая не принимает аргументов и ничего не возвращает, и вызывает ее. У него нет способа узнать наверняка, что такое" say_hi1 " - предварительное соглашение-это все, что удерживает нас от segfaulting.

то, что я показал выше, - это семейство функций 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 скомпилирован как общая библиотека с именем " спам.Итак", мы можем предоставить этот initspam() функция:

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

если имя функции init не соответствует имени файла, интерпретатор Python не может знать, как его найти. Например, переименование спама.так что в notspam.поэтому и попытка импорта дает следующее.

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

если соглашение об именах нарушено, просто неизвестно, содержит ли общая библиотека функцию инициализации.

второе ключевое соглашение заключается в том, что после вызова функция init отвечает за инициализацию себя, вызывая Py_InitModule. этот вызов добавляет модуль в "словарь" / хэш-таблицу, хранящуюся интерпретатором, который сопоставляет имя модуля с данными модуля. Он также регистрирует функции C в таблице методов. После вызова Py_InitModule модули могут инициализировать себя другими способами, такими как добавление объектов. (Пример: объект SpamError в учебнике API Python C). (Py_InitModule на самом деле макрос, который создает реальный вызов инициализации, но с некоторая информация запечена в том, какую версию Python использовало наше скомпилированное расширение C.)

если функция 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 имел смысл динамически loaded C. таблица методов представляет собой массив struct PyMethodDef с конечной записью sentinel. 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, которые принимают дополнительные аргументы, могут быть "украдены" путем приведения к PyCFunction. (См. keywdarg_parrot пример Python C API учебник.) Даже функции C, которые резервируют функции Python, которые не принимают аргументов в Python, будут принимать два аргумента в C (показано ниже). Все функции должны возвращать что-то (что может быть объектом нет). Функции, которые принимают несколько позиционных аргументов в Python должны распаковать эти аргументы из одного объекта в с.

вот как получают данные для взаимодействия с динамически загруженными функциями C и на хранении. Наконец, вот пример того, как используются эти данные.

контекст здесь заключается в том, что мы оцениваем Python "opcodes", инструкцию по инструкции, и мы попали в код вызова функции. (см. https://docs.python.org/2/library/dis.html. Стоит обезжиренного.) Мы определили, что объект функции Python поддерживается функцией C. В приведенном ниже коде мы проверяем, не принимает ли функция в Python никаких аргументов (в Python), и если да, вызовите ее (с двумя аргументами в С.)

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,он получает аргумент self. В нижней части вы можете видеть, что meth назначается указатель функции, который затем разыменовывается и вызывается. Возвращаемое значение заканчивается на x.