2025, Dec 07 18:03
Как встраивать CPython в C++‑DLL на Windows и не зависеть от vcpkg
Как упаковать CPython с C++‑DLL на Windows: PYTHONHOME, Lib/DLLs, обход ошибки ModuleNotFoundError: encodings, загрузка python311.dll и риски безопасности.
Встраивать CPython в C++‑DLL на Windows кажется делом простым — пока сборка работает только на машине разработчика и рассыпается на других. Характерный признак — ModuleNotFoundError: No module named 'encodings' после переноса бинарников из каталога решения. Первопричина не в вызовах API, а в устройстве рантайма и механизме, по которому CPython находит стандартную библиотеку. Ниже — практическое объяснение, почему так происходит, и как упаковать Python так, чтобы DLL запускалась где угодно и не зависела от vcpkg_installed.
Минимальное встраивание, которое «работает» в дереве сборки
Ниже фрагмент, который инициализирует интерпретатор и выполняет вызов в Python. Рядом с разработческой раскладкой он срабатывает, потому что жестко прописывает пути, взятые из vcpkg_installed.
void ensurePyBootstrapped() {
if (!Py_IsInitialized()) {
PyConfig cfg;
PyConfig_InitPythonConfig(&cfg);
wchar_t modPath[MAX_PATH];
GetModuleFileNameW(NULL, modPath, MAX_PATH);
std::wstring modDir = modPath;
size_t offs = modDir.find_last_of(L"\\");
if (offs != std::wstring::npos) {
modDir = modDir.substr(0, offs);
}
std::wstring homeDir = modDir + L"\\..\\..\\vcpkg_installed\\x64-windows-static-md\\x64-windows-static-md\\tools\\python3";
wchar_t absBuf[MAX_PATH];
GetFullPathNameW(homeDir.c_str(), MAX_PATH, absBuf, NULL);
homeDir = absBuf;
std::wstring libDir = homeDir + L"\\Lib";
std::wstring sitePkgs = libDir + L"\\site-packages";
std::wstring dllDir = homeDir + L"\\DLLs";
PyConfig_SetString(&cfg, &cfg.home, homeDir.c_str());
std::wstring searchPath = libDir + L";" + sitePkgs + L";" + dllDir;
PyConfig_SetString(&cfg, &cfg.pythonpath_env, searchPath.c_str());
PyStatus st = Py_InitializeFromConfig(&cfg);
PyConfig_Clear(&cfg);
if (PyStatus_Exception(st)) {
PyErr_Print();
return;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(\".\")");
}
}
void EXPORTED_API pyEcho(const char* msg) {
ensurePyBootstrapped();
PyObject* s = PyUnicode_FromString(msg);
if (s) {
PyObject* builtins = PyEval_GetBuiltins();
PyObject* printFn = PyDict_GetItemString(builtins, "print");
if (printFn && PyCallable_Check(printFn)) {
PyObject* args = PyTuple_Pack(1, s);
PyObject_CallObject(printFn, args);
Py_DECREF(args);
}
Py_DECREF(s);
}
}
Клиентское приложение:
#include <iostream>
#include "py_embed.h"
int main() {
std::cout << "Hello";
pyEcho("Hello from Python");
}
Что идет не так и почему
CPython не становится самодостаточным лишь от того, что вы кладете рядом python3.dll и python312.dll. Интерпретатору нужна стандартная библиотека в PYTHONHOME (его можно переопределить через конфигурационные API). Когда запуск идет рядом с vcpkg_installed, каталоги Lib, Lib\site-packages и DLLs доступны, поэтому импорты вроде encodings разрешаются. Перенесите бинарники в другое место — и эти директории исчезнут; интерпретатор стартует, но сразу падает на импорте encodings. Сообщение о sys.path и ошибке — прямое следствие того, что PYTHONHOME указывает на путь, существующий только внутри дерева сборки.
Есть еще одно практическое ограничение в Windows. Загрузчик процесса должен найти python311.dll или python312.dll в момент загрузки. Если положить Python‑DLL рядом с вашей DLL, загрузчик будет доволен. Если перенести Python‑DLL в подкаталог, придется заранее прописать этот путь в PATH либо загружать библиотеку явно через LoadLibrary вместо прямого линкования.
Сделайте DLL независимой от vcpkg_installed
Надежный вариант — поставлять интерпретатор вместе с приложением и инициализировать CPython на упакованном рантайме. Установите PYTHONHOME до вызова Py_InitializeFromConfig и направьте его на каталог python, который идет в комплекте с вашими бинарниками. Так делают многие настольные приложения.
.
└── app_root/
├── app.exe
├── python311.dll
└── python/
├── DLLs/
│ ├── _ctypes.pyd
│ └── ...
├── Lib/
│ ├── site_packages/
│ │ └── numpy, etc...
│ ├── os.py
│ └── ...
└── bin (optional)/
├── python311.dll (see note below)
└── python.exe (see note below)
Если python311.dll лежит не рядом с исполняемым файлом, а в подкаталоге, скорректируйте PATH или загрузите её через LoadLibrary, избегая прямого линкования. Если положить в комплект python.exe, пользователи смогут ставить дополнительные библиотеки через pip, запуская python.exe -m pip install ...
Исправленная инициализация, указывающая на ваш упакованный Python
Эта инициализация берет за якорь каталог исполняемого файла и задает PYTHONHOME равным app_root\python. Остальная логика прежняя, но жесткая привязка к дереву vcpkg_installed исчезает.
void initEmbeddedPython() {
if (!Py_IsInitialized()) {
PyConfig cfg;
PyConfig_InitPythonConfig(&cfg);
wchar_t exeBuf[MAX_PATH];
GetModuleFileNameW(NULL, exeBuf, MAX_PATH);
std::wstring baseDir = exeBuf;
size_t cut = baseDir.find_last_of(L"\\");
if (cut != std::wstring::npos) {
baseDir = baseDir.substr(0, cut);
}
std::wstring pkgHome = baseDir + L"\\python";
std::wstring libDir = pkgHome + L"\\Lib";
std::wstring sitePkgs = libDir + L"\\site-packages";
std::wstring dllDir = pkgHome + L"\\DLLs";
PyConfig_SetString(&cfg, &cfg.home, pkgHome.c_str());
std::wstring searchPath = libDir + L";" + sitePkgs + L";" + dllDir;
PyConfig_SetString(&cfg, &cfg.pythonpath_env, searchPath.c_str());
PyStatus st = Py_InitializeFromConfig(&cfg);
PyConfig_Clear(&cfg);
if (PyStatus_Exception(st)) {
PyErr_Print();
return;
}
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.path.append(\".\")");
}
}
void EXPORTED_API pyEcho(const char* msg) {
initEmbeddedPython();
PyObject* s = PyUnicode_FromString(msg);
if (s) {
PyObject* builtins = PyEval_GetBuiltins();
PyObject* printFn = PyDict_GetItemString(builtins, "print");
if (printFn && PyCallable_Check(printFn)) {
PyObject* args = PyTuple_Pack(1, s);
PyObject_CallObject(printFn, args);
Py_DECREF(args);
}
Py_DECREF(s);
}
}
С такой раскладкой интерпретатор находит encodings и остальную стандартную библиотеку в упакованной папке python, независимо от системы сборки. Python‑DLL должна лежать рядом с вашим EXE или DLL, чтобы удовлетворить загрузчик Windows, если только вы не используете обходной путь через PATH или LoadLibrary.
Когда полноценный CPython — избыточен
Если вам нужна лишь разборка или выполнение синтаксиса Python для легкого скриптинга и не требуются C/C++‑расширения вроде numpy, присмотритесь к другим реализациям: RustPython, IronPython, JYThon или pocketpy. Их можно встраивать без комплектования стандартной библиотеки CPython, но экосистему C‑расширений CPython они не поддерживают.
Компромиссы по безопасности, о которых стоит помнить
Упаковка Python означает, что стандартная библиотека и ваши скрипты лежат на диске рядом с приложением. Пользователи могут их читать и менять. Это исключает хранение секретов и создает риск внедрения кода через локальные правки. Снизить возможность подмены помогает кастомный импортер, который подгружает код из памяти, а не с диска, хотя нативные расширения усложняют этот подход. Даже в этом случае решительный атакующий сможет реверс‑инжинирить решение.
Почему это важно
Без стандартной библиотеки в PYTHONHOME интерпретатор срывается на первом импорте — как в случае с encodings. Понимая ожидания рантайма CPython, вы сможете собрать воспроизводимый пакет для развёртывания, а не эксперимент, зависящий от дерева сборки. Это также объясняет, почему раздача «одной DLL» нереалистична, если вы используете CPython и C‑расширения.
Итоги
Надежная схема встраивания Python в C++‑DLL на Windows — поставлять интерпретатор и стандартную библиотеку вместе с приложением, направлять PYTHONHOME на этот пакет и инициализировать через Py_InitializeFromConfig. Держите Python‑DLL там, где её найдет загрузчик, либо загружайте явно. Если экосистема CPython не нужна, альтернативные интерпретаторы упростят дистрибуцию ценой совместимости с расширениями. Учитывайте последствия для безопасности от поставки читаемого и изменяемого кода Python и выбирайте компромиссы под свою задачу.