Предисловие

Привет, читатель! Меня зовут Александр Щербанюк, и я Python-разработчик. Это первая статья цикла, который посвящен разбору внутреннего устройства CPython.

На написание этой статьи, как и на желание разобраться во внутреннем устройстве CPython, меня вдохновила книга Энтони Шоу "Внутри CPython. Гид по интерпретатору Python" (обложку см. ниже).

Крайне рекомендую ее к прочтению, если тебя интересует, что скрывается под капотом Python. Несмотря на то, что книга действительно заслуживает внимания, она не лишена недостатков, самый значительный из которых - это достаточно верхнеуровневое описание некоторых этапов работы CPython. Например, этапов предконфигурации и самой конфигурации. Именно эти "темные пятна" мотивировали меня начать разбираться с тем, как же всё-таки работает CPython.

В чем цель статей?

Цель статей - разобраться на достаточно глубоком уровне, как работает CPython. То есть, что происходит от момента ввода в консоли команды python my_script.py, до момента окончания её работы. Каждая статья будет разбирать и описывать очередной кусочек флоу работы CPython, формируя таким образом непрерывное повествование о его внутреннем устройстве и процессе работы.

Как будут выглядеть статьи?

Эта и все последующие статьи будут состоять из трех простых блоков. Первый блок - небольшое предисловие. Как минимум оно будет содержать ссылку на предыдущую статью, для удобства навигации и "сшивания" всех статей воедино. Второй блок - основное тело статьи. Оно будет содержать описание логически завершенного кусочка флоу CPython. Третий блок - итоги. Этот блок будет содержать краткие и ключевые результаты рассмотренной в статье логики CPython.

Итак, начнем.

Настройка VS Code

Чтобы начать разбираться во внутреннем устройстве CPython, необходимо скачать его исходники:

git clone https://github.com/python/cpython.git

Здесь и далее я будут использовать IDE Visual Studio Code, ввиду чего дополнительно необходимо установить официальное расширение, предназначенное для отладки и удобства навигации по исходным файлам 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.
После успешной сборки в корневой директории появятся, в моем случае, две папки: 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++.

На этом подготовительные мероприятия окончены, и можно перейти к погружению в исходный код 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 или Py_BytesMain) из 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 из 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

Комментарии (0)