- PVSM.RU - https://www.pvsm.ru -
Привет, читатель! Меня зовут Александр Щербанюк, и я Python-разработчик. Это первая статья цикла, который посвящен разбору внутреннего устройства CPython.
На написание этой статьи, как и на желание разобраться во внутреннем устройстве CPython, меня вдохновила книга Энтони Шоу "Внутри CPython. Гид по интерпретатору Python" (обложку см. ниже).

Крайне рекомендую ее к прочтению, если тебя интересует, что скрывается под капотом Python. Несмотря на то, что книга действительно заслуживает внимания, она не лишена недостатков, самый значительный из которых - это достаточно верхнеуровневое описание некоторых этапов работы CPython. Например, этапов предконфигурации и самой конфигурации. Именно эти "темные пятна" мотивировали меня начать разбираться с тем, как же всё-таки работает CPython.
Цель статей - разобраться на достаточно глубоком уровне, как работает CPython. То есть, что происходит от момента ввода в консоли команды python my_script.py, до момента окончания её работы. Каждая статья будет разбирать и описывать очередной кусочек флоу работы CPython, формируя таким образом непрерывное повествование о его внутреннем устройстве и процессе работы.
Эта и все последующие статьи будут состоять из трех простых блоков. Первый блок - небольшое предисловие. Как минимум оно будет содержать ссылку на предыдущую статью, для удобства навигации и "сшивания" всех статей воедино. Второй блок - основное тело статьи. Оно будет содержать описание логически завершенного кусочка флоу CPython. Третий блок - итоги. Этот блок будет содержать краткие и ключевые результаты рассмотренной в статье логики CPython.
Итак, начнем.
Чтобы начать разбираться во внутреннем устройстве CPython, необходимо скачать его исходники:
git clone https://github.com/python/cpython.git
Здесь и далее я будут использовать IDE Visual Studio Code [1], ввиду чего дополнительно необходимо установить официальное расширение [2], предназначенное для отладки и удобства навигации по исходным файлам CPython.
Затем, из корневой папки склонированного ранее репозитория необходимо выполнить команду, с помощью которой фиксируется рассматриваемая версия CPython:
git checkout 3.13
Весь цикл статей будет основываться на версии 3.13 (hash commit: 606022c55db3b0b0a226747bc928a836d613f9f6).
Далее, необходимо выполнить несколько команд для конфигурации и компиляции CPython:
./configure --prefix=$(pwd)/cross-build/macOS
make -j10 all
make install
В моем случае сборка осуществляется под платформу macOS. Команды сборки под Unix и Windows немного отличаются и могут быть найдены в Python Developer's Guide [3].
После успешной сборки в корневой директории появятся, в моем случае, две папки: build/ и cross-build/macOS/. Именно в последней будет расположен бинарный файл CPython, который мы будем использовать для отладки.
И последний оставшийся шаг - настройка процесса отладки в VS Code. Для этого необходимо создать файл ./.vscode/launch.json следующего содержания:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug CPython",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/cross-build/macOS/bin/python3",
"args": ["-c", "print('Hello from CPython!')"],
"stopAtEntry": true,
"cwd": "${workspaceFolder}",
"externalConsole": false,
"MIMode": "lldb",
},
],
}
Это минимальная жизнеспособная конфигурация отладчика. Важно отметить, что в качестве значения параметра configurations[0].program используется путь до бинарника, который был собран на шаге ранее. В зависимости от платформы, на которой будет осуществляться отладка, представленные выше настройки могут отличаться. За особенностями конфигурации дебаггера для других платформ можно обратиться к официальной документации VS Code по настройке отладки C/C++ [4].
На этом подготовительные мероприятия окончены, и можно перейти к погружению в исходный код CPython.
После запуска отладки выполнение сразу же останавливается во входной точке, которой является функция main, расположенная в файле Programs/python.c:
#include "Python.h"
#ifdef MS_WINDOWS
int wmain(int argc, wchar_t **argv) {
return Py_Main(argc, argv);
}
#else
int main(int argc, char **argv) {
return Py_BytesMain(argc, argv);
}
#endif
Из листинга видно, что в зависимости от платформы вызываются разные стартовые функции: для Windows вызывается функция wmain(), а для всего прочего (Linux, MacOS, и др.) - main(). Это связано с разностью кодировок на разных платформах: в Windows для передачи аргументов командной строки используется кодировка UTF-16, а для прочих платформ (не всегда, но в подавляющем большинстве случаев) - UTF-8. Так или иначе, в аргументе argv оказываются все аргументы, переданные через командную строку, а в argc - их количество. Таким образом, после выполнения команды ./cross-build/macOS/bin/python3 -c "print('Hello from CPython!')" в argv окажется ./cross-build/macOS/bin/python3 -c "print('Hello from CPython!')", а в argc - 3.
Как устроен
argv, и почему вargcбудет3?
Если присмотреться кargvчуть пристальнее, можно заметить, что эта переменная является двумерным массивом символов, и, на самом деле, каждая его строка содержит отдельный аргумент. То есть:
argv[0] = "./cross-build/macOS/bin/python3"
argv[1] = "-c"
argv[2] = "print('Hello from CPython!')"Именно поэтому в
argcбудет лежать3.
Затем следует одна из двух платформозависимых функций (Py_Main [5] или Py_BytesMain [6]) из Modules/main.c:
int Py_Main(int argc, wchar_t **argv) { // для Windows
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 0,
.bytes_argv = NULL,
.wchar_argv = argv
};
return pymain_main(&args);
}
int Py_BytesMain(int argc, char **argv) { // для прочих платформ
_PyArgv args = {
.argc = argc,
.use_bytes_argv = 1,
.bytes_argv = argv,
.wchar_argv = NULL
};
return pymain_main(&args);
}
Единственной задачей этих функций является формирование объекта типа _PyArgv из Include/internal/pycore_initconfig.h:
typedef struct _PyArgv {
Py_ssize_t argc;
int use_bytes_argv;
char * const *bytes_argv;
wchar_t * const *wchar_argv;
} _PyArgv;
Структура агрегирует входные параметры командной строки: их количество, разнообразие форматов (char * const */wchar_t * const *) и флаг (use_bytes_argv) того, какой из двух форматов был реально использован.
Следом, вне зависимости от платформы, вызывается функция pymain_main из Modules/main.c:
static int pymain_main(_PyArgv *args) {
PyStatus status = pymain_init(args);
if (_PyStatus_IS_EXIT(status)) {
pymain_free();
return status.exitcode;
}
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}
return Py_RunMain();
}
Эта функция интересна тем, что в ней в первый раз используется структура PyStatus [7] из Include/cpython/initconfig.h. Это универсальный тип, предназначенный для сохранения информации о статусах отработки различных инициализационных функций внутри CPython. Описание структуры представлено ниже:
typedef struct {
enum {
_PyStatus_TYPE_OK=0,
_PyStatus_TYPE_ERROR=1,
_PyStatus_TYPE_EXIT=2
} _type;
const char *func;
const char *err_msg;
int exitcode;
} PyStatus;
Структура состоит из следующих полей:
_type - тип статуса:
_PyStatus_TYPE_OK - удачное завершение функции
_PyStatus_TYPE_ERROR - в процессе работы функции возникла ошибка
_PyStatus_TYPE_EXIT - функция запросила завершение процесса
func - имя функции, спровоцировавшей ошибку. Будет отлично от NULL только в случае _type = _PyStatus_TYPE_ERROR.
err_msg - текст ошибки. Будет отлично от NULL только в случае _type = _PyStatus_TYPE_ERROR.
exitcode - код, с которым должен завершиться процесс. Будет отлично от NULL только в случае _type = _PyStatus_TYPE_EXIT.
Популярность
PyStatus
PyStatusактивно используется как возвращаемый тип многих функций инициализации CPython. Так, его можно встретить более 1000 раз в более чем 70 различных исходных файлах CPython.
Затем, поток выполнения переходит к функции pymain_init(), определенной в Modules/main.c:
static PyStatus pymain_init(const _PyArgv *args) {
PyStatus status;
status = _PyRuntime_Initialize();
if (_PyStatus_EXCEPTION(status)) {
return status;
}
PyPreConfig preconfig;
PyPreConfig_InitPythonConfig(&preconfig);
status = _Py_PreInitializeFromPyArgv(&preconfig, args);
if (_PyStatus_EXCEPTION(status)) {
return status;
}
PyConfig config;
PyConfig_InitPythonConfig(&config);
if (args->use_bytes_argv) {
status = PyConfig_SetBytesArgv(&config, args->argc, args->bytes_argv);
}
else {
status = PyConfig_SetArgv(&config, args->argc, args->wchar_argv);
}
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = Py_InitializeFromConfig(&config);
if (_PyStatus_EXCEPTION(status)) {
goto done;
}
status = _PyStatus_OK();
done:
PyConfig_Clear(&config);
return status;
}
}
Далее мы подробно рассмотрим назначение и интересные особенности большинства из представленных функций, погружаясь в глубокие детали их реализации лишь по необходимости.
В статье был описаны два первых шага на пути погружения во внутренности CPython. Во-первых, была настроена IDE для пошагового выполнения CPython. Во-вторых, была найдена точка входа в CPython и рассмотрены несколько стартовых функций с используемыми в них структурами.
Мой ТГ-контакт для связи: https://t.me/AlexandrShherbanjuk [8]
Автор: soul701
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/434964
Ссылки в тексте:
[1] Visual Studio Code: https://code.visualstudio.com/
[2] официальное расширение: https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools
[3] Python Developer's Guide: https://devguide.python.org/getting-started/setup-building/#compile-and-build
[4] настройке отладки C/C++: https://code.visualstudio.com/docs/cpp/launch-json-reference
[5] Py_Main: https://docs.python.org/3/c-api/init.html#c.Py_Main
[6] Py_BytesMain: https://docs.python.org/3/c-api/init.html#c.Py_BytesMain
[7] PyStatus: https://docs.python.org/3.13/c-api/init_config.html#c.PyStatus
[8] https://t.me/AlexandrShherbanjuk: https://t.me/AlexandrShherbanjuk
[9] Источник: https://habr.com/ru/articles/961220/?utm_source=habrahabr&utm_medium=rss&utm_campaign=961220
Нажмите здесь для печати.