Давеча потребовалось встроить интерпретатор Python-а в C++ приложение.
Поскольку в проекте уже использовался vcpkg, то часть проблем отпала сама собой: подтянуть Python3 в проект и слинковаться с ним особой проблемы не составило. Правда пришлось в CMakeLists.txt для приложения, в которое Python3 вставлялся, добавить несколько строчек, чтобы на Linux-е к приложению линковалось бы еще и библиотеки util и dl.
Под Linux-ом результирующий бинарник даже сразу запустился. А вот под Windows при старте приложение выдавало ошибку типа вот такой:
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'
и все, никакой нормальной работы.
Как я понял, проблема в том, что когда Python стартует, то он пытается разыскать свою стандартную библиотеку (т.е. туеву хучу *.py файлов). И под Windows он ее найти не может.
В процессе разбирательства выяснилось интересное. Когда мы работаем в Linux-е, то vcpkg при компиляции Python3 раскидывает компоненты Python следующим образом (cmake-build -- это каталог, в котором идет сборка проекта):
cmake-build/
`- vcpkg_installed/
`- x64-linux/
`- ...
`- lib/
`- ...
`- python3.10/
`- *.py # Тут у нас stdlib для Python3.
`- ...
`- tools/
`- python3/
`- python # А тут сам Python3.
`- ...
При этом в результирующий бинарник каким-то образом хардкодятся абсолютные пути к стандартной библиотеке Python-а. Т.е. что-то вроде:
/home/eao197/prj/cmake-build/vcpkg_installed/lib/python3.10
Соответственно, если я удалю свой cmake-build или перенесу результат компиляции на другую машину с Linux-ом, где никакого vcpkg_installed нет, то приложение не сможет стартовать с той же самой ошибкой.
А вот в Windows результат компиляции Python3 размещается по-другому:
cmake-build/
`- vcpkg_installed/
`- x64-windows-static/
`- ...
`- lib # lib есть, но Python-а там нет.
`- tools/
`- python3/
`- lib/
`- *.py # Тут у нас stdlib для Python3.
`- python # А тут сам Python3.
`- ...
и результирующий бинарник про расположение стандартной библиотеки Python3 вообще ничего не знает.
Соответственно, выход виделся в том, чтобы расположить стандартную библиотеку Python3 прямо рядом с результирующим бинарником. Чтобы было что-то вроде:
`- bin/
`- python3-lib/
`- *.py # Тут у нас stdlib для Python3.
`- my_app # А тут само приложение.
Чтобы разобраться с этой проблемой нужно было решить два вопроса.
Первый вопрос: как при компиляции приложения разместить рядом с ним стандартную библиотеку Python3?
Этот вопрос решился тем, что в проектный CMakeLists.txt для my_app добавилось еще одно правило по копированию стандартной библиотеки Python3 в нужное мне место. Что-то типа:
add_custom_command(
TARGET ${MY_APP}
POST_BUILD
COMMAND ${CMAKE_COMMAND}
-E copy_directory ${PYTHON3_LIBRARY_SOURCE_DIR} ${PYTHON3_LIBRARY_DEST_DIR}
)
Правда при этом пришлось вычислять расположение стандартной библиотеки в зависимости от платформы. Как-то так:
find_package(Python3 COMPONENTS Development REQUIRED)
get_filename_component(PYTHON3_EXECUTABLE_DIR
${Python3_EXECUTABLE} DIRECTORY)
if(WIN32)
set(PYTHON3_LIBRARY_SOURCE_DIR ${PYTHON3_EXECUTABLE_DIR}/lib)
else()
set(PYTHON3_LIBRARY_SOURCE_DIR ${PYTHON3_EXECUTABLE_DIR}/../../lib/python3.10)
endif()
set(PYTHON3_LIBRARY_DEST_DIR ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/python3-lib)
Плюс к тому, я не в курсе как в CMake проверить, что python3-lib уже скопирован и не делать копию еще раз. В итоге этот самый custom-command заставляет копировать стандартную библиотеку каждый раз когда приложение собирается.
Upd. Под Linux-ом нужно озаботиться еще и передачей линкеру опций -Xlinker -export-dynamic, причины смотрим здесь. Так что в CMake для проекта нужно добавить еще что-то вроде:
if(NOT WIN32)
target_link_options(${MY_APP} PRIVATE -Xlinker -export-dynamic)
endif()
Второй вопрос: как при запуске интерпретатора Python-а указать ему где искать стандартную библиотеку?
Этот вопрос решился как-то вот так (Upd. на самом деле нужно добавлять несколько путей к библиотекам):
Py_SetPythonHome( program_path );
PyStatus status;
PyConfig config;
PyConfig_InitIsolatedConfig( &config );
std::unique_ptr< PyConfig, PyConfig_cleaner > config_cleaner{ &config };
for( const auto & additional_path : {
L"/python3-lib",
L"/python3-lib/lib-dynload",
L"/python3-lib/site-packages" } )
{
status = PyWideStringList_Append(
&config.module_search_paths,
(std::wstring{ program_path } + additional_path).c_str() );
if( PyStatus_Exception( status ) )
{
throw std::runtime_error{...};
}
}
config.module_search_paths_set = 1;
status = Py_InitializeFromConfig( &config );
if( PyStatus_Exception( status ) )
{
throw std::runtime_error{...};
}
Т.е. сперва вручную задается PYTHONHOME, а затем в конфигурации для Python-а добавляется подкаталог "python3-lib".
Уж не знаю, насколько все это корректно, но оно хотя бы работает.
При этом в проекте планируется использовать pybind11 и первоначально я хотел запускать Python через pybind11::scoped_interpreter. Но проблема в том, что конструктор scoped_interpreter сам запускает Python управляя его конфигурацией, а это не позволяет мне задать пути поиска стандартной библиотеки Python-а.
Поэтому пришлось делать свой аналог scoped_interpreter, который в конструкторе выполняет нужные мне действия, а вот в деструкторе вызывает pybind11::finalize_interpreter.
Но и это было еще не все :(
Отдельных хлопот добавило формирование значения, которое отдается в вызов Py_SetPythonHome. Дело в том, что туда нужно отдать wchar_t*, тогда как main в приложении получает argv как char ** и путь к программе вычисляется в виде объекта std::filesystem::path.
Под Windows все решилось так: у std::filesystem::path вызывается wstring() и получившийся std::wstring уже отдается в Py_SetPythonHome. По крайней мере в VC++ все это работает без проблем, в том числе когда в пути есть имена каталогов русскими буквами.
А вот под Linux-ом попытка вызвать std::filesystem::path::wstring() для пути, содержащего в себе русские буквы, приводит к выбросу исключения. Видимо, чего-то в реализации стандартной библиотеки от GCC не хватает.
При этом значение argv[0] в Linux-е задается в UTF-8 поэтому, в общем-то, ничего не мешает преобразовать UTF-8 в голый юникод и представить результат в виде std::wstring. Но...
Но, как оказалось, Python-у не нравится, когда ему в wchar_t* отдают голый Unicode. Такое ощущение, что он ждет значение в каком-то другом представлении, типа UTF-16 или UTF-32.
Поэтому под Linux значение из std::filesystem::path::c_str() нужно отдать в Py_DecodeLocale, а уже результат Py_DecodeLocale следует передавать в Py_SetPythonHome. Тогда работает как ожидается.
Странно, что под Windows использовать Py_DecodeLocale у меня не получилось: возвращенное им значение как-то неадекватно воспринимается Py_SetPythonHome. Такое ощущение, что под Windows нужно именно то представление, которое возвращается std::filesystem::path::wstring() (т.е. UCS-2).
Вот как-то так. Не без приключений.
Если кто-то знает как внедрить Python3 в C++ приложение более прямым способом, то поделитесь рецептом, плз.
Мне же в разбирательстве помогли следующие ссылки:
- https://stackoverflow.com/questions/71398303/running-embedded-python-in-c-using-vcpkg-to-install. Вопрос на StackOverflow о том, что делать с ошибкой "Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding".
- https://github.com/matusnovak/python-embedded-example-project. Небольшой демо-проект с CMake-файлами, показывающий как внедрить Python3 в приложение (но без vcpkg, а через git submodules).
В очередной раз вынужден сказать, что CMake -- это редкостное говно. Мне обидно быть коллегой тех говнокодеров, которые высрали это на свет божий. Периодически хочется послать C++ куда подальше и уйти, пусть даже в Rust. Но сильнее всего это желание проявляется когда приходится погружаться в потроха CMake-овских скриптов.
Комментариев нет:
Отправить комментарий