9

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

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

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

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

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

+1

Существует много информации о загрузке модуля в [документации системы импорта] (https://docs.python.org/3/reference/import.html) – GWW

+0

Это хороший вопрос; Мне сейчас интересно. Я бы пожертвовал больше, если бы не стремился к 10 тыс. – Veedrac

+1

Python ищет несколько разных имен для любого данного модуля; вы можете прочитать об этой части процесса здесь: http://stackoverflow.com/questions/6319379/python-shared-object-module-naming-convention –

ответ

12

TLDR короткая версия жирный.

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

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

Исторически C-расширения для Python были статически связаны с интерпретатором Python. Это потребовало, чтобы пользователи Python перекомпилировали интерпретатор каждый раз, когда хотели использовать новый модуль, написанный на C. Как вы можете себе представить, и как Guido van Rossum describes, это стало непрактичным, поскольку сообщество росло. Сегодня большинство пользователей Python никогда не компилируют интерпретатор один раз. Мы просто «pip install 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"); 
} 

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

Что я показал выше, это семейство функций 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 {имя_модуля} и просит компоновщик для него. Начиная с этого момента Python опирается на небольшой набор соглашений, чтобы сделать динамическую загрузку C-кода возможной и надежной.

Давайте посмотрим, какие расширения C должны выполнить для выполнения контракта, который делает вышеуказанный вызов dlsym(). Для скомпилированных модулей C Python первое соглашение, позволяющее Python обращаться к скомпилированному C-коду, является функцией init {shared_library_filename}(). Для a module named spam скомпилирована как разделяемую библиотеку с именем «spam.so», мы могли бы обеспечить эту функцию initspam():

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

Если имя функции инициализации не соответствует имени файла, интерпретатор 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, но с некоторой информацией, запеченной как в том, какая версия 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, чтобы иметь смысл динамически загружен 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? Это ЬурейеЕ, также включать/methodobject.h

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

PyCFunction является ЬурейиМ для указателя на функцию, которая возвращает указатель на PyObject и который принимает аргументы два указателя на PyObjects. В качестве леммы к конвенции три функции C, зарегистрированные в таблице методов, имеют одну и ту же подпись.

Python обходит большую часть трудности при динамической нагрузке с использованием ограниченного набора сигнатур функций C. Одна подпись, в частности, используется для большинства функций C. Указатели на функции C, которые принимают дополнительные аргументы, могут быть «прокручены» путем литья в PyCFunction. (См. Пример keywdarg_parrot в Python C API tutorial.) Даже функции C, которые резервируют функции Python, которые не принимают аргументов в Python, будут принимать два аргумента в 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, он получает аргумент self. Внизу вы можете видеть, что meth назначается указатель на функцию, который затем разыменовывается и вызывается. Возвращаемое значение заканчивается на x.

+2

+1 Это очень тщательный ответ. – refi64

+0

Если кто-то все еще читает это ... можно ли сказать, что встроенные C-модули статически связаны в двоичном коде python, а пользовательские расширения динамически связаны? – SamuelN

+1

@SamuelN Вы можете получить список модулей, статически связанных между собой, путем импорта 'sys' и проверки' sys.builtin_module_names'. Многие, казалось бы, фундаментальные модули, которые поставляются с Python, такие как 'math', не являются строго« встроенными »и фактически динамически загружаются. Вы можете увидеть это, импортировав 'math' и проверив' math .__ file__'. Модули, которые встроены и статически связаны, являются модулями, которые очень тесно интегрированы с интерпретатором, таким как 'sys'. (Этот ответ немного предварительный. Прошло некоторое время с тех пор, как я просмотрел его, и я все еще проверяю дважды). – Praxeolitic

Смежные вопросы