ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [C++] Embedding Python + CMake
    기타 코딩 2024. 3. 2. 13:27

    이번 글에서는 C++에서 Python C API를 사용하여 파이썬 인터프리터를 C++에 내장시키는 방법과 이를 빌드하기 위한 CMake 스크립트를 소개하고, Boost.Python 라이브러리를 소개해볼것 이다.

     

    0.Requirement

    빌드에 사용한 환경은 다음과 같다.

    • Windows 10 
    • Visual Studio Commuity 2022 (Command Line만 사용하니 Visual Studio BuildTool을 사용해도 된다.)
    • MSVC v143
    • Window SDK 10.0.22621.0
    • Python 3.11.7 (64bit)
    • CMake minimum require 3.18
    • Ninja

    초보자를 위한 설명을 덧붙이자면, 빌드 작업은 Developer Command Prompt에서 진행해야하며, 설치된 파이썬의 비트에 맞추어 Developer Command Prompt를 설정해주어야 한다. 64비트인 경우, vcvarsall.bat 또는 vcvar64.bat을 사용하여 64비트 환경에 맞추어 설정을 해주거나 처음부터 64비트 Native 툴에서 진행하면 된다. 또한 nmake 하게되면 visual studio project가 생성되기때문에 cmake + nmake + visual studio 총 세번 빌드를 해야하므로 귀찮아 진다.

     

    1.Python Embedding에 필요한 파일들

    • (파이썬이 설치된 폴더)/include

    해당 헤더파일은 API을 사용하기 위해 include 경로에 추가 (컴파일러 I옵션)해주어야 한다.

    • python(버전).lib

    아래에서 설명할 python(버전).dll을 링크하기 위해 필요한 lib 파일이다.

    python3.lib는 사용되지 않는 듯 하다.

    • python(버전).dll

    파이썬 API가 구현되어 있는 dll 파일이다. 프로그램을 실행시킬 때, 프로그램이 있는 경로에서 먼저 dll을 찾고, 없는 경우

    환경 변수 Path에 있는 디렉터리에서 찾는데, 발견하지 못하면 프로그램이 실행되지 않는다. 때문에 다른 사람에게 배포를 하는 경우라면 반드시, dll을 복사해서 실행파일과 같은 디렉터리에 넣어주어야 한다. 또한 컴퓨터 내에 여러 파이썬이 설치되어 있고, 환경변수에 등록된 파이썬과 사용한 파이썬의 버전이 다른 경우도 같은 디렉터리에 넣어주어야 한다.

     

    • DLLs & Lib

    DLLs는 파이썬에서 사용하는 dll(pyd 확장자 형태, 리눅스는 so)이 있는 폴더이며, Lib는 os, sys, random과 같은 파이썬의 기본 내장 모듈이 있는 곳이다. 이들 역시 별다른 설정이 없다면, 기본경로는 실행파일과 같은 디렉터리 밑 DLLs와 Lib에 있어야 한다. 이를 바꾸고 싶다면 환경변수 PYTHONHOME을 지정하거나, API 함수를 통해 지정해주어야 한다.

     

    2.Python API 기본 패턴

    아래는 공식 API에서 제공하는 예제이다.

    #define PY_SSIZE_T_CLEAN
    #include <Python.h>
    
    int main(int argc, char *argv[]) {
        wchar_t *program = Py_DecodeLocale(argv[0], NULL);
        if (program == NULL) {
            fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
            exit(1);
        }
        Py_SetProgramName(program);  /* optional but recommended */
        Py_Initialize();
        PyRun_SimpleString("from time import time,ctime\n"
                           "print('Today is', ctime(time()))\n");
        if (Py_FinalizeEx() < 0) {
            exit(120);
        }
        PyMem_RawFree(program);
        return 0;
      }

    https://docs.python.org/ko/3/extending/embedding.html

     

    1. Embedding Python in Another Application

    The previous chapters discussed how to extend Python, that is, how to extend the functionality of Python by attaching a library of C functions to it. It is also possible to do it the other way arou...

    docs.python.org

    Python.h를 include하면 자동으로 stdio를 포함한 몇가지 기본 c 라이브러리를 include 한다. PY_SSIZE_T_CLEAN은 공식문서에서 Python.h를 include 하기전 매번 선언할 것을 추천한다. 그런데 이대로 디버그 빌드에서 컴파일한다면 Cannot open file 'python311_d.lib'라는 오류가 발생한다. _d는 디버그용 lib로 배포되는 Release 빌드에는 없으며, 직접 CPython을 빌드해야 얻을 수 있다. 그런데 CPython까지 디버깅해야할 필요는 없으므로 아래와같은 방법으로 디버그 빌드에서도 Release 파이썬 lib를 링크하도록 하는 것을 추천한다.

    #define PY_SSIZE_T_CLEAN
    #ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
    #else
    #include <Python.h>
    #endif

    마지막으로 Py_Initialize()로 인터프리터를 초기화시키고, PyFinalizeEx()로 인터프리터가 할당한 모드 메모리를 해제한 후, PyDecodeLocale이 할당하였던 메모리까지 해제해주어야 한다.

    참고로 PyFinalize()는 구버전과 호환성을 위해 남겨둔 함수이며, PyDecodeLocale은 argv[0]를 parsing하여 실행하는 프로그램이름을 반환한다. program 이름을 설정하지 않는 경우 이름은 python이 된다.

     

    3.Python Initialization for Embeded Environment 

    Py_Initialize 사용시 sys.path

    Py_Initialize()로 초기화하는 것은 임베딩 환경에 적합하지 않다. 왜냐하면 운영체제의 환경변수의 영향을 받아 의도치 않은 동작을 할 수 있기 때문이다. 대표적으로 Path에서 파이썬을 발견한 경우 해당 파이썬을 기준으로 sys.path에 추가되어 버리며, 환경변수의 PYTHONPATH와 PYTHONHOME을 통해서도 동작이 바뀌기 때문이다. PYTHONHOME을 통해서 원하는 파이썬 위치를 설정을 할 수 있지 않나고 물을 수도 있겠지만, Windows API의 SetEnvironmentVariable나 msvc에서 구현된 POSIX 함수인 putenv로는 파이썬 인터프리터로 PYTHONHOME이 전달되지 않는다. 다행이 파이썬 API에는 Py_Initialize()가 수행하는 동작을 프로그래머가 설정할 수 있도록 Py_InitializeFromConfig이라는 함수를 제공하며, 임베딩된 상황에 적합하게 config을 초기화해주는 PyConfig_InitIsolatedConfig 함수가 있다. Isolated 상태이면 운영체제의 환경변수는 초기화과정에서 무시하게 된다. 보다 자세한 내용은 아래 공식 API 문서 참고

    https://docs.python.org/ko/3/c-api/init_config.html#python-path-configuration

     

    Python Initialization Configuration

    Python can be initialized with Py_InitializeFromConfig() and the PyConfig structure. It can be preinitialized with Py_PreInitialize() and the PyPreConfig structure. There are two kinds of configura...

    docs.python.org

    //생략
    #include <filesystem>
    namespace fs = std::filesystem;
    
    PyStatus init_python(const wchar_t* program) {
        //API Reference https://docs.python.org/ko/3/c-api/init_config.html#python-path-configuration
        
        //get python absolute path
        fs::path python_path = fs::canonical(fs::path("./python"));
        
        PyStatus status;
        PyConfig config;
        PyConfig_InitIsolatedConfig(&config);
        try {
            //set program name
            status = PyConfig_SetString(&config, &config.program_name, program);
            if(PyStatus_Exception(status)) throw status;
    
            // Read all configuration at once to verify
            status = PyConfig_Read(&config);
            if(PyStatus_Exception(status)) throw status;
    
            //set python_home
            status = PyConfig_SetString(&config, &config.home, python_path.wstring().c_str());
            if(PyStatus_Exception(status)) throw status;
    
            //set sys.path explictly
            config.module_search_paths_set = 1;
            std::array<fs::path, 4> path_arr = {fs::path("./python/DLLs"), fs::path("./python/Lib"), fs::path("./python/Lib/site-packages"), fs::path("./python-scripts")};
            for(auto iter : path_arr) {
                status = PyWideStringList_Append(&config.module_search_paths, fs::canonical(iter).wstring().c_str());
                if(PyStatus_Exception(status)) throw status;
            }
    
            status = Py_InitializeFromConfig(&config);
            PyConfig_Clear(&config);
            return status;
        }
        catch(PyStatus& status) {
            PyConfig_Clear(&config);
            return status;
        }
    }

    Py_SetProgramname 대신 config을 통해서 프로그램 이름을 설정해야하며, PyConfig_SetString대신 사용할 수 있는 PyConfig_SetBytesString은 내부적으로 Py_DecodeLocale을 사용한다. 또한 아래와 같이 디렉터리를 구성하기 위해 module_serach_paths를 사용하였다. 만약 이를 설정하지 않는다면, 설정된 home에 맞추어서 기본 sys.path를 추가한다.

    디렉터리 구성
    python 폴더내 디렉터리 구성

    참고로 Lib에 있는 파이썬 기본 모듈중에 임베딩에 사용되지 않는 모듈도 많이 있으니, 배포 용량을 줄이고 프로그램을 가볍게 하고 싶다면, 안쓰는 걸 찾아내서 삭제하면 된다.

    4.파이썬 라이브러리 사용하기 (pip)

    기본적으로 pip로 라이브러리를 설치하면 (파이썬이 설치된 폴더)/Lib/site-packages에 설치되는데, 임베딩 상황이라면 별도의 폴더에다 설치해주어야 한다. pip의 -t 또는 --target 옵션을 사용하면 설치될 라이브러리의 위치를 지정할 수 있으며, -r 옵션을 사용하면 txt 파일에서 필요한 라이브러리 목록을 읽어올 수 있다. (후술할 cmake를 사용할 때, 사용할 패키지가 추가되어도 다시 cmake를 안해도되어 편리하다.) 이때 Lib/site-packages 이외의 곳에 라이브러리를 설치한다면 sys.path에 해당 경로가 추가되어 있어야 한다.

    python -m pip install -r requirement.txt -target <path>

    numpy를 설치한 상황

    배포 용량을 줄이고 싶다면 저기서 numpy 폴더 빼고 모두 삭제하면 된다.

    5.C++과 Python 사이 데이터 교환

    PyRun_SimpleString이나 PyRun_Simplefile을 사용하면 python 코드나 py 파일을 실행시킬 수 있지만, 데이터 교환이 불가능하다는 단점이 있다. 데이터를 교환하기 위해서는 API 함수를 통해 파이썬 코드를 실행할 필요가 있으며, 공통적인 흐름은 다음과 같다.

    1. 모듈 (.py 파일) import -> PyImport_Import(PyObject* name) or PyImport_ImportModule(const char* name)
    2. 해당 모듈에 정의된 Attribute 레퍼런스 얻기 -> PyObject_GetAttr(PyObject* o, PyObject* name) or PyObject_GetAttrString(PyObject* o, const char* name)
    3. PyObject_Call, PyObject_CallObject, PyList_SetItem과 같은 함수로 객체 사용
    4. Py_DECREF를 통해 레퍼런스 카운트를 감소시키는 것으로 메모리 정리

    파이썬은 모든 것이 객체로 되어있는 언어로 모듈이든 함수이든 리스트이든 모두 PyObject 구조체의 포인터에 동적 할당되어 저장된다. 또한 객체를 누구도 온전히 소유하지 않으며, 객체에 대한 Reference (즉 PyObject 구조체의 포인터)만 소유할 수 있다. 이때 파이썬은 Reference Count 기반의 Garbage Collection으로 동적 할당된 객체의 메모리를 회수하지만, API에서는 사용자가 직접 Py_INCREF와 Py_DECREF로 카운트를 조절해주어야 한다. 다시말해 객체를 복사해서 가져오면 Py_INCREF로 카운트를 올려주고, 소멸할 때는 Py_DECREF로 카운트로 내려주어야 한다. Py_DECREF는 카운트가 0인 객체에 대해서 메모리 할당을 해제하는 역할도 겸하므로, 카운트 관리를 잘못하였다면 해제된 메모리에 접근하여 문제가 발생할 수 있다. 또한 PyObject를 얻는데 실패하면 NULL을 반환하므로 이부분에 대한 예외처리까지 해주어야 한다.

     

    마지막으로 함수나 __call__이 정의된 callable 객체를 call하기 위해서는 파이썬의 *args와 **kwargs와 같이 positional arument는 tuple로 keyward argument는 dictionary로 전달해주어야 한다.

     

    #my_module.py
    def say_hello():
        print("Hello Embedded Python")
    
    def multiple(a, b):
        return a*b

    아래는 numpy와 이 py 파일을 C++에서 실행시키는 예제이다.

    //Python_Wraps.h
    #ifndef __PYTHON_WRAPS__
    #define __PYTHON_WRAPS__
    
    #include <string>
    #include <stdexcept>
    
    #define PY_SSIZE_T_CLEAN
    #ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
    #else
    #include <Python.h>
    #endif
    
    namespace PyWraps {
        namespace Error {
            class PyObjectNotFound : public std::exception {
                public:
                const char* what() const override {return "Error: Cannot find PyObject";}
            };
    
            class PyModuleNotFound : public std::exception {
                public:
                const char* what() const override {return "Error: Cannot find Module";}
            };
    
            class PyOjectConvertError : public std::exception {
                public:
                const char* what() const override {return "Error: Cannot convert PyObject";}
            };
    
            class PyMemAllocError : public std::exception {
                public:
                const char* what() const override {return "Error: Fail to allocate memory";}
            };
    
            class PyObjectCallError : public std::exception {
                public:
                const char* what() const override {return "Error: Fail to call object";}
            };
        }
    
        class PyCallable {
            private:
            PyObject* pObject;
    
            public:
            PyCallable(PyObject* pModule, std::string name);
            ~PyCallable() {Py_DECREF(pObject);}
            PyObject* operator()(PyObject** pValue);
            PyObject* operator()(PyObject** pValue, PyObject* args);
            PyObject* operator()(PyObject** pValue, PyObject* args, PyObject* pKwargs);
        };
        
    };
    
    #endif
    //Python_Wraps.cpp
    #include "Python_Wraps.h"
    
    PyWraps::PyCallable::PyCallable(PyObject *pModule, std::string name) {
        pObject = PyObject_GetAttrString(pModule, name.c_str());
        if(!pObject || !PyCallable_Check(pObject)) throw PyWraps::Error::PyObjectNotFound();
    }
    
    PyObject *PyWraps::PyCallable::operator()(PyObject **pValue) {
        *pValue = PyObject_CallNoArgs(pObject);
        return *pValue;
    }
    
    PyObject *PyWraps::PyCallable::operator()(PyObject **pValue, PyObject *pArgs) {
        *pValue = PyObject_CallObject(pObject, pArgs);
        return *pValue;
    }
    
    PyObject *PyWraps::PyCallable::operator()(PyObject **pValue, PyObject *pArgs, PyObject *pKwargs) {
        *pValue = PyObject_Call(pObject, pArgs, pKwargs);
        return *pValue;
    }
    #include <iostream>
    #include <string>
    #include <array>
    #include <filesystem>
    namespace fs = std::filesystem;
    
    #include <wchar.h>
    #include<conio.h>
    
    #define PY_SSIZE_T_CLEAN
    #ifdef _DEBUG
    #undef _DEBUG
    #include <Python.h>
    #define _DEBUG
    #else
    #include <Python.h>
    #endif
    
    #include "Python_Wraps.h"
    
    /// @brief Initialized python interpreter on isolated mode
    /// @param program program name using PyDecodeLocale
    /// @return PyStatus
    PyStatus init_python(const wchar_t* program) {
        //API Reference https://docs.python.org/ko/3/c-api/init_config.html#python-path-configuration
        
        //get python absolute path
        fs::path python_path = fs::canonical(fs::path("./python"));
        
        PyStatus status;
        PyConfig config;
        PyConfig_InitIsolatedConfig(&config);
        try {
            //set program name
            status = PyConfig_SetString(&config, &config.program_name, program);
            if(PyStatus_Exception(status)) throw status;
    
            // Read all configuration at once to verify
            status = PyConfig_Read(&config);
            if(PyStatus_Exception(status)) throw status;
    
            //set python_home
            status = PyConfig_SetString(&config, &config.home, python_path.wstring().c_str());
            if(PyStatus_Exception(status)) throw status;
    
            //set sys.path explictly
            config.module_search_paths_set = 1;
            std::array<fs::path, 4> path_arr = {fs::path("./python/DLLs"), fs::path("./python/Lib"), fs::path("./python/Lib/site-packages"), fs::path("./python-scripts")};
            for(auto iter : path_arr) {
                status = PyWideStringList_Append(&config.module_search_paths, fs::canonical(iter).wstring().c_str());
                if(PyStatus_Exception(status)) throw status;
            }
    
            status = Py_InitializeFromConfig(&config);
            PyConfig_Clear(&config);
            return status;
        }
        catch(PyStatus& status) {
            PyConfig_Clear(&config);
            return status;
        }
    }
    
    int main(int argc, char* argv[]) {
        wchar_t *program = Py_DecodeLocale(argv[0], NULL);
        if(program == NULL) {
            std::cerr << "Fatal Error: cannot decode argv[0]" << std::endl;
            exit(1);
        }
        init_python(program);
        #ifdef _DEBUG
        PyRun_SimpleString("import sys");
    
        std::cout << "======sys.path======" << std::endl;
        PyRun_SimpleString("print(sys.path)");
        #endif
    
        std::cout << "\n======numpy test======" << std::endl;
        PyRun_SimpleString("import numpy as np");
        std::cout << "print(np.array([1, 2, 3]))" << std::endl;
        PyRun_SimpleString("print(np.array([1, 2, 3]))");
    
        std::cout << "\n======Call functions in my_module.py======" <<std::endl;
        //https://docs.python.org/3/c-api/import.html
        //https://docs.python.org/3/c-api/call.html#object-calling-api
        //https://docs.python.org/ko/3/extending/embedding.html
        PyObject *pModule = nullptr;
        PyObject *pArgs = nullptr;
        PyObject *pValue = nullptr;
        try {
            std::cout << "import my_module" << std::endl;
            pModule = PyImport_ImportModule("my_module");
            if(!pModule) throw PyWraps::Error::PyModuleNotFound();
    
            std::cout << "excute say_hello" << std::endl;
            PyWraps::PyCallable fsay_hello(pModule, "say_hello");
            fsay_hello(&pValue);
    
            std::cout << "create tuple for args" << std::endl;
            pArgs = PyTuple_New(2);
            if(!pArgs) throw PyWraps::Error::PyMemAllocError();
    
            std::cout << "add 2.0 on tuple" << std::endl;
            pValue = PyFloat_FromDouble(2.0);
            if(!pValue) throw PyWraps::Error::PyOjectConvertError();
            PyTuple_SetItem(pArgs, 0, pValue); //pValue reference stolen here
            pValue = nullptr;
            
            std::cout << "add 3.0 on tuple" << std::endl;
            pValue = PyFloat_FromDouble(3.0);
            if(!pValue) throw PyWraps::Error::PyOjectConvertError();
            PyTuple_SetItem(pArgs, 1, pValue); //pValue reference stolen here
            pValue = nullptr;
    
            std::cout << "excute mutiple" << std::endl;
            PyWraps::PyCallable fmultiple(pModule, "multiple");
            fmultiple(&pValue, pArgs);
            if(!pValue) throw PyWraps::Error::PyObjectCallError();
            std::cout << "Result of Call: " << PyFloat_AsDouble(pValue) << std::endl;
            Py_DECREF(pValue);
            Py_DECREF(pArgs);
            Py_DECREF(pModule);
        }
        catch(std::exception& e) {
            std::cout << e.what() << std::endl;
            if(pValue) {
                Py_DECREF(pValue);
                pValue = nullptr;
            }
            if(pArgs) {
                Py_DECREF(pArgs);
                pArgs = nullptr;
            }
            if(pModule) {
                Py_DECREF(pModule);
                pArgs = nullptr;
            }
    
        }
        
        if(Py_FinalizeEx() < 0) {
            exit(120);
        }
        PyMem_RawFree(program);
        std::cout << "\nPress any key to exit" << std::endl;
        _getch();
        return 0;
    }

    실행 결과

    6.CMake로 빌드 환경 구축하기 + 파이썬 스크립트

    매번 컴파일러에 긴 파이썬 include path를 인수로 전달하기는 힘들 것이다. 여기에 설치된 파이썬으로부터 dll, Lib 등을 복사하고, 소스의 파이썬 코드를 build된 장소에 옮겨주기 위해 Cmake와 파이썬 스크립트를 사용하여 빌드 환경을 구축하였다.

    메인 CMakeLists.txt

    cmake_minimum_required(VERSION 3.18)
    
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    set(CMAKE_CXX_EXTENSIONS OFF) #disable extened syntex by compiler
    
    project(HelloEmbed)
    
    #https://cmake.org/cmake/help/latest/module/FindPython.html
    find_package(Python REQUIRED Development Interpreter)
    
    set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
    
    add_subdirectory(src)
    add_subdirectory(python-scripts)
    
    #install python Lib
    add_custom_target(
        installStdLib
        COMMENT "Install python DLLs and Lib (Source: ${Python_ROOT})"
        DEPENDS Python::Interpreter
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/build-scripts
        COMMAND ${Python_EXECUTABLE} install-python-lib.py ${Python_ROOT} ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}
    )
    add_dependencies(${CMAKE_PROJECT_NAME} installStdLib)
    
    add_custom_target(
        installLib
        COMMENT "Install python site-packages"
        DEPENDS Python::Interpreter
        COMMAND ${Python_EXECUTABLE} -m pip install -r ${CMAKE_SOURCE_DIR}/build-scripts/requirements.txt --target ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}/python/Lib/site-packages --no-warn-script-location
    )
    add_dependencies(installLib installStdLib)

    filesystem을 사용하였기 때문에 c++17 표준으로 지정하였으며, find_package로 파이썬을 찾는다. 만약 파이썬이 Path에 없어서 자동으로 찾지 못한다면 -D Python_ROOT_DIR=(path)로 직접 파이썬 경로를 지정해주면 된다.

    그리고 VSCode에서 InteliSense설정을 위해  set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)로 compile_commands.json이 생성되게 만들었다.

    -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE

    옵션을 통해서도 생성할 수 있으니 참고

    c_cpp_properties.json

     

    src/CMakeLists.txt

    file(GLOB_RECURSE SRC_FILES CONFIGURE_DEPENDS
            ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
    )
    
    add_executable(${CMAKE_PROJECT_NAME} ${SRC_FILES})
    add_dependencies(${CMAKE_PROJECT_NAME} Python::Interpreter)
    
    target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${Python_INCLUDE_DIRS})
    target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${Python_LIBRARIES})
    
    set_target_properties(${CMAKE_PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME})
    
    if(MSVC)
            target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE /W4 /utf-8)
    else()
            target_compile_options(${CMAKE_PROJECT_NAME} PRIVATE -Wall)
    endif()

    파이썬 안 include 디렉터리를 target_include_directories로 지정하였으며, target_link_libararies로 python311.lib를 링크한다. Python_INCLUDE_DIRS와 Python_LIBRARIES는 find_package로 Python Development를 찾은 경우 자동으로 등록되는 변수이다.

     

    python-scripts/CMakeLists.txt

    add_custom_target(
        installScript
        COMMENT "Copy Python Script"
        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/build-scripts
        COMMAND ${Python_EXECUTABLE} sync-scripts.py ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}/python-scripts
    )
    add_dependencies(${CMAKE_PROJECT_NAME} installScript)

     

    빌드 보조용 파이썬 스크립트

    cmake는 리눅스처럼 경로는 /로 구분하기 때문에 cmake에서 전달되는 경로는 윈도우 cmd에서 사용하면 제대로 작동하지 않는 것이 있다. (대표적으로 xcopy) 따라서 파이썬 스크립트로 이를 대체하였다.

    #install-python-lib.py
    import sys, os, shutil, functools
    
    def install_logger(name):
        def real_logger(func):
            @functools.wraps(func)
            def warrper(*args, **kwargs):
                print("Install", name, flush=True)
                func(*args, **kwargs)
                print("Done", flush=True)
            return warrper
        return real_logger
    
    @install_logger("DLLs")
    def install_DLLS(python_path:str, build_path:str):
        src = os.path.join(python_path, "DLLs")
        dist = os.path.join(build_path, "python/DLLs")
        if (not os.path.isdir(dist)) and os.path.isdir(src):
            shutil.copytree(src, dist)
    
    @install_logger("Lib")
    def install_Lib(python_path:str, build_path:str):
        src = os.path.join(python_path, "Lib")
        dist = os.path.join(build_path, "python/Lib")
        if os.path.isdir(dist):
            return
        
        if not os.path.isdir(src):
            print("Error: cannot find the paths", flush=True)
            return
            
        os.mkdir(dist)
        os.mkdir(os.path.join(dist, "site-packages"))
        names = os.listdir(src)
        try:
            names.remove("site-packages")
            names.remove("__pycache__")
        except ValueError:
            pass
        
        size = len(names)
        for i in range(size):
            target = os.path.join(src, names[i])
            if os.path.isfile(target):
                shutil.copy(target, dist)
                
            elif os.path.isdir(target):
                shutil.copytree(target, os.path.join(dist, names[i]))
            
            print(f"Copy[{i+1}/{size}]", names[i], flush=True)
    
    if __name__ == "__main__":
        python_path = sys.argv[1]
        build_path = sys.argv[2]
    
        install_DLLS(python_path, build_path)
        install_Lib(python_path, build_path)
        dllfile = os.path.join(python_path, "python311.dll")
        if os.path.isfile(dllfile):
            shutil.copy(dllfile, build_path)

    파이썬의 DLLs와 Lib를 복사하는 스크립트이다. 이때 Lib의 __pycache__와 site-packages는 제외하였다.

     

    configure_file(<Input> <Output>)
    
    #configure_file로 생성한 파일들이 자동으로 Include directory에 포함되게 하려면
    set(CMAKE_INCLUDE_CURRENT_DIR ON)

     

    이걸 이용해서 프로그램 버전등을 설정할 수도 있으며, 설정이 담긴 헤더파일이 빌드 경로에 생성되도록 할 수 있다.

    자세한 정보는 아래 공식 문서 참고

    https://cmake.org/cmake/help/latest/command/configure_file.html

     

    configure_file — CMake 3.29.0-rc4 Documentation

    Contents Copy a file to another location and modify its contents. configure_file( [NO_SOURCE_PERMISSIONS | USE_SOURCE_PERMISSIONS | FILE_PERMISSIONS ...] [COPYONLY] [ESCAPE_QUOTES] [@ONLY] [NEWLINE_STYLE [UNIX|DOS|WIN32|LF|CRLF] ]) Copies an file to an fil

    cmake.org

    #sync-scripts.py
    import sys, os, shutil
    
    src_path = sys.argv[1]
    dest_path = sys.argv[2]
    
    if not __name__ == "__main__" or not os.path.isdir(src_path):
        sys.exit() 
    
    if not os.path.isdir(dest_path):
        os.mkdir(dest_path)
    
    names = os.listdir(src_path)
    try:
        names.remove("CMakeLists.txt")
    except ValueError:
        pass
    
    for name in names:
        origin_file = os.path.join(src_path, name)
        dest_file = os.path.join(dest_path, name)
        if not os.path.isfile(dest_file) or os.path.getmtime(origin_file) > os.path.getmtime(dest_file):
            print("Copy", origin_file)
            shutil.copy(origin_file, dest_file)
    
    print("Done")

     

     

     python-scripts안에 작성한 py파일과 build폴더안의  python-scripts의 수정시각을 비교하여 동기화하는 스크립트이다.

    파일을 첨부한다.

    Hello_Embeding_Python.zip
    0.01MB

    Build 과정

    64비트로 설정하여 진행하였다. 32비트로 설정된 상태에서는 64비트 파이썬을 찾지 못하니 주의

    cmake -G Ninja -B build -DCMAKE_BUILD_TYPE:STRING=Debug -DPython_ROOT_DIR="C:\Program Files\Python311"
    cd build
    ninja all
    ninja installLib

    출력

    C:\Users\HJ\Desktop\Hello_Embeding_Python>cmake -G Ninja -B build -DCMAKE_BUILD_TYPE:STRING=Debug -DPython_ROOT="C:\Program Files\Python311"
    -- The C compiler identification is MSVC 19.36.32535.0
    -- The CXX compiler identification is MSVC 19.36.32535.0
    -- Detecting C compiler ABI info
    -- Detecting C compiler ABI info - done
    -- Check for working C compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.36.32532/bin/Hostx64/x64/cl.exe - skipped
    -- Detecting C compile features
    -- Detecting C compile features - done
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Check for working CXX compiler: C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/MSVC/14.36.32532/bin/Hostx64/x64/cl.exe - skipped
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Found Python: C:/Program Files/Python311/python.exe (found version "3.11.7") found components: Development Interpreter Development.Module Development.Embed 
    -- Configuring done
    -- Generating done
    -- Build files have been written to: C:/Users/HJ/Desktop/Hello_Embeding_Python/build
    
    C:\Users\HJ\Desktop\Hello_Embeding_Python>cd build
    
    C:\Users\HJ\Desktop\Hello_Embeding_Python\build>ninja all
    [0/2] Re-checking globbed directories...
    [1/5] Copy Python Script
    Copy C:/Users/HJ/Desktop/Hello_Embeding_Python/python-scripts\my_module.py
    Done
    [2/5] Install python DLLs and Lib (Source: C:\Program Files\Python311)
    Install DLLs
    Done
    Install Lib
    Copy[1/198] abc.py
    Copy[2/198] aifc.py
    Copy[3/198] antigravity.py
    Copy[4/198] argparse.py
    Copy[5/198] ast.py
    Copy[6/198] asynchat.py
    Copy[7/198] asyncio
    Copy[8/198] asyncore.py
    Copy[9/198] base64.py
    Copy[10/198] bdb.py
    Copy[11/198] bisect.py
    Copy[12/198] bz2.py
    Copy[13/198] calendar.py
    Copy[14/198] cgi.py
    Copy[15/198] cgitb.py
    Copy[16/198] chunk.py
    Copy[17/198] cmd.py
    Copy[18/198] code.py
    Copy[19/198] codecs.py
    Copy[20/198] codeop.py
    Copy[21/198] collections
    Copy[22/198] colorsys.py
    Copy[23/198] compileall.py
    Copy[24/198] concurrent
    Copy[25/198] configparser.py
    Copy[26/198] contextlib.py
    Copy[27/198] contextvars.py
    Copy[28/198] copy.py
    Copy[29/198] copyreg.py
    Copy[30/198] cProfile.py
    Copy[31/198] crypt.py
    Copy[32/198] csv.py
    Copy[33/198] ctypes
    Copy[34/198] curses
    Copy[35/198] dataclasses.py
    Copy[36/198] datetime.py
    Copy[37/198] dbm
    Copy[38/198] decimal.py
    Copy[39/198] difflib.py
    Copy[40/198] dis.py
    Copy[41/198] distutils
    Copy[42/198] doctest.py
    Copy[43/198] email
    Copy[44/198] encodings
    Copy[45/198] ensurepip
    Copy[46/198] enum.py
    Copy[47/198] filecmp.py
    Copy[48/198] fileinput.py
    Copy[49/198] fnmatch.py
    Copy[50/198] fractions.py
    Copy[51/198] ftplib.py
    Copy[52/198] functools.py
    Copy[53/198] genericpath.py
    Copy[54/198] getopt.py
    Copy[55/198] getpass.py
    Copy[56/198] gettext.py
    Copy[57/198] glob.py
    Copy[58/198] graphlib.py
    Copy[59/198] gzip.py
    Copy[60/198] hashlib.py
    Copy[61/198] heapq.py
    Copy[62/198] hmac.py
    Copy[63/198] html
    Copy[64/198] http
    Copy[65/198] imaplib.py
    Copy[66/198] imghdr.py
    Copy[67/198] imp.py
    Copy[68/198] importlib
    Copy[69/198] inspect.py
    Copy[70/198] io.py
    Copy[71/198] ipaddress.py
    Copy[72/198] json
    Copy[73/198] keyword.py
    Copy[74/198] lib2to3
    Copy[75/198] linecache.py
    Copy[76/198] locale.py
    Copy[77/198] logging
    Copy[78/198] lzma.py
    Copy[79/198] mailbox.py
    Copy[80/198] mailcap.py
    Copy[81/198] mimetypes.py
    Copy[82/198] modulefinder.py
    Copy[83/198] msilib
    Copy[84/198] multiprocessing
    Copy[85/198] netrc.py
    Copy[86/198] nntplib.py
    Copy[87/198] ntpath.py
    Copy[88/198] nturl2path.py
    Copy[89/198] numbers.py
    Copy[90/198] opcode.py
    Copy[91/198] operator.py
    Copy[92/198] optparse.py
    Copy[93/198] os.py
    Copy[94/198] pathlib.py
    Copy[95/198] pdb.py
    Copy[96/198] pickle.py
    Copy[97/198] pickletools.py
    Copy[98/198] pipes.py
    Copy[99/198] pkgutil.py
    Copy[100/198] platform.py
    Copy[101/198] plistlib.py
    Copy[102/198] poplib.py
    Copy[103/198] posixpath.py
    Copy[104/198] pprint.py
    Copy[105/198] profile.py
    Copy[106/198] pstats.py
    Copy[107/198] pty.py
    Copy[108/198] pyclbr.py
    Copy[109/198] pydoc.py
    Copy[110/198] pydoc_data
    Copy[111/198] py_compile.py
    Copy[112/198] queue.py
    Copy[113/198] quopri.py
    Copy[114/198] random.py
    Copy[115/198] re
    Copy[116/198] reprlib.py
    Copy[117/198] rlcompleter.py
    Copy[118/198] runpy.py
    Copy[119/198] sched.py
    Copy[120/198] secrets.py
    Copy[121/198] selectors.py
    Copy[122/198] shelve.py
    Copy[123/198] shlex.py
    Copy[124/198] shutil.py
    Copy[125/198] signal.py
    Copy[126/198] site.py
    Copy[127/198] smtpd.py
    Copy[128/198] smtplib.py
    Copy[129/198] sndhdr.py
    Copy[130/198] socket.py
    Copy[131/198] socketserver.py
    Copy[132/198] sqlite3
    Copy[133/198] sre_compile.py
    Copy[134/198] sre_constants.py
    Copy[135/198] sre_parse.py
    Copy[136/198] ssl.py
    Copy[137/198] stat.py
    Copy[138/198] statistics.py
    Copy[139/198] string.py
    Copy[140/198] stringprep.py
    Copy[141/198] struct.py
    Copy[142/198] subprocess.py
    Copy[143/198] sunau.py
    Copy[144/198] symtable.py
    Copy[145/198] sysconfig.py
    Copy[146/198] tabnanny.py
    Copy[147/198] tarfile.py
    Copy[148/198] telnetlib.py
    Copy[149/198] tempfile.py
    Copy[150/198] test
    Copy[151/198] textwrap.py
    Copy[152/198] this.py
    Copy[153/198] threading.py
    Copy[154/198] timeit.py
    Copy[155/198] token.py
    Copy[156/198] tokenize.py
    Copy[157/198] tomllib
    Copy[158/198] trace.py
    Copy[159/198] traceback.py
    Copy[160/198] tracemalloc.py
    Copy[161/198] tty.py
    Copy[162/198] turtle.py
    Copy[163/198] types.py
    Copy[164/198] typing.py
    Copy[165/198] unittest
    Copy[166/198] urllib
    Copy[167/198] uu.py
    Copy[168/198] uuid.py
    Copy[169/198] venv
    Copy[170/198] warnings.py
    Copy[171/198] wave.py
    Copy[172/198] weakref.py
    Copy[173/198] webbrowser.py
    Copy[174/198] wsgiref
    Copy[175/198] xdrlib.py
    Copy[176/198] xml
    Copy[177/198] xmlrpc
    Copy[178/198] zipapp.py
    Copy[179/198] zipfile.py
    Copy[180/198] zipimport.py
    Copy[181/198] zoneinfo
    Copy[182/198] _aix_support.py
    Copy[183/198] _bootsubprocess.py
    Copy[184/198] _collections_abc.py
    Copy[185/198] _compat_pickle.py
    Copy[186/198] _compression.py
    Copy[187/198] _markupbase.py
    Copy[188/198] _osx_support.py
    Copy[189/198] _pydecimal.py
    Copy[190/198] _pyio.py
    Copy[191/198] _py_abc.py
    Copy[192/198] _sitebuiltins.py
    Copy[193/198] _strptime.py
    Copy[194/198] _threading_local.py
    Copy[195/198] _weakrefset.py
    Copy[196/198] __future__.py
    Copy[197/198] __hello__.py
    Copy[198/198] __phello__
    Done
    [4/5] Building CXX object src\CMakeFiles\HelloEmbed.dir\main.cpp.obj
    C:\Users\HJ\Desktop\Hello_Embeding_Python\src\main.cpp(63): warning C4100: 'argc': 참조되지 않은 정식 매개 변수입니다.
    [5/5] Linking CXX executable HelloEmbed\HelloEmbed.exe
    
    C:\Users\HJ\Desktop\Hello_Embeding_Python\build>ninja installLib
    [0/2] Re-checking globbed directories...
    [1/2] Install python DLLs and Lib (Source: C:\Program Files\Python311)
    Install DLLs
    Done
    Install Lib
    Done
    [2/2] Install python site-packages
    Collecting numpy (from -r C:/Users/HJ/Desktop/Hello_Embeding_Python/build-scripts/requirements.txt (line 1))
      Using cached numpy-1.26.4-cp311-cp311-win_amd64.whl.metadata (61 kB)
    Using cached numpy-1.26.4-cp311-cp311-win_amd64.whl (15.8 MB)
    DEPRECATION: Loading egg at c:\program files\python311\lib\site-packages\vboxapi-1.0-py3.11.egg is deprecated. pip 24.3 will enforce this behaviour change. A possible replacement is to use pip for package installation.. Discussion can be found at https://github.com/pypa/pip/issues/12330
    Installing collected packages: numpy
    ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
    tensorflow-intel 2.12.0 requires numpy<1.24,>=1.22, but you have numpy 1.26.4 which is incompatible.
    Successfully installed numpy-1.26.4

     

    7.Warping Libraries

    이렇게 수동으로 Refernce count를 관리하기 힘든데, PyObject를 class로 wraping한 다음, 소멸자와 복사 대입 연산자, 이동 대입 연산자 등을 정의하면 자동으로 Refernce count를 관리할 수 있을 것이다. 이를 직접 구현할 필요는 없으며, boost나 pybind11같은 라이브러리를 사용하면 된다. 간단하게 보여주기 위해 그냥 Py_Initialize 함수를 사용하였다.

    Boost.Python

    //main.cpp
    #include <iostream>
    //기본값은 동적 링크, 정적 링크로 하고 싶으면
    //#define BOOST_PYTHON_STATIC_LIB
    #include <boost/python.hpp>
    namespace bp = boost::python;
    
    int main(int argc, char* argv[]) {
        Py_Initialize();
        try{
            std::cout <<"import builtins" << std::endl;
            auto module_builtins = bp::import("builtins");
            auto print_func = module_builtins.attr("print");
    
            auto msg_pystr = bp::str("Hello");
            std::cout <<"call print" << std::endl;
            print_func(msg_pystr, "\nhello2", 123);
    
            auto sum_func = module_builtins.attr("sum");
            std::cout << "call sum(1, 2)" << std::endl;
            auto value = sum_func(bp::make_tuple(1, 2));
            std::cout << "result of call: " << PyFloat_AsDouble(value.ptr()) << std::endl;
        }
        catch (bp::error_already_set) {
            PyErr_Print();
        }
        Py_Finalize();
        return 0;
    }

    출력, 동적 링크

    사용되는 python311.dll과 boost_python dll은 찾아서 복붙하거나, 경로가 환경 변수 Path에 추가되어 있어야 한다.

     

    boost에서 기본적인 python tuple, list, str 등은 warping 해두었기 때문에 편하게 사용할 수 있으며, 객체가 가지고 있는 PyObject*를 받으려면 ptr 매서드를 사용하면 된다. 

    https://wiki.python.org/moin/boost.python/EmbeddingPython

     

    boost.python/EmbeddingPython - Python Wiki

    New Revisions of Boost Since this Wiki page was originally written, Boost::Python has added C++ wrappers for a lot of the direct C code this page references. The documentation for those wrappers are available under "Embedding" in the TOC. The current versi

    wiki.python.org

     

    만약 Boost에서 Python API를 직접 사용하고 싶다면,  PyObject를 boost::python::object로 wraping하여 사용하며, boost::python::handle<>를 통해 PyObject의 Refernce count를 관리하면 된다.

    https://wiki.changwoo.pe.kr/research:embeddedpython

     

    research:embeddedpython [ChangwooWiki]

    Embedded Python 개괄 파이썬이 다른 언어와 어떻게 잘 어울릴 수 있는지 증명하는 과정? 파이썬으로도 좋은 GUI를 만들 수 있겠지만, 시스템쪽으로 Qt library를 사용하기로 결정하였고, 이것으로 사용

    wiki.changwoo.pe.kr

     

    추가) global namespace에 있는 객체 가져오기

    std::cout << "\ntest global attr" <<std::endl;
    PyRun_SimpleString("a=1");
    PyRun_SimpleString("print('a is', a, 'in python')");
    std::cout << "import __main__" << std::endl;
    auto module_main = bp::import("__main__");
    bp::object a = module_main.attr("a");
    std::cout << "a is " << PyFloat_AsDouble(a.ptr()) << " in c++" << std::endl;

    파이썬 global namespace에 선언한 객체는 __main__ 모듈에서 가져올 수 있다.

     

    빌드 환경

    #CMakeLists.txt
    cmake_minimum_required(VERSION 3.18)
    
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    set(CMAKE_CXX_EXTENSIONS OFF)
    
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    
    project(HelloBP)
    
    find_package(Python REQUIRED Development)
    if(${Python_FOUND})
        message("Found Python ${Python_VERSION}")
        message("Python include ${Python_INCLUDE_DIRS}")
        message("Python lib ${Python_LIBRARIES}")
    endif()
    
    find_package(Boost REQUIRED COMPONENTS python${Python_VERSION_MAJOR}${Python_VERSION_MINOR}) 
    
    if(${Boost_FOUND})
        message("Found Boost ${Boost_VERSION}")
        message("Boost include: ${Boost_INCLUDE_DIRS}")
        message("Boost lib dir: ${Boost_LIBRARY_DIRS}")
        message("Boost lib: ${Boost_LIBRARIES}")
    endif()
    
    add_executable(${CMAKE_PROJECT_NAME} main.cpp)
    
    target_include_directories(${CMAKE_PROJECT_NAME}
        PRIVATE ${Boost_INCLUDE_DIRS}
        PRIVATE ${Python_INCLUDE_DIRS}
        )
    
    target_link_libraries(${CMAKE_PROJECT_NAME}
        PRIVATE ${Python_LIBRARIES}
        PRIVATE ${Boost_LIBRARIES}
        )

    추가) cmake version >= 3.2 인 경우

    cmake 3.2 부터 <Library_Name>_ROOT cmake 변수 or 환경 변수를 사용하려면, 아래와 같이 CMP0074 policy를 NEW로 설정해주어야 한다.

    cmake_policy(SET CMP0074 NEW)

    NEW 동작은 이러한 변수들을 사용하는 것이며, OLD 동작은 이러한 변수들을 무시하는 것이다. 공식 문서를 보니 이러한 기능이 3.12부터 등장한 것 같은데, cmake_minimum_required를 바꾸어가면서 실험한 결과 3.2부터 policy를 설정해야하는 것을 알 수 있었다.

    https://cmake.org/cmake/help/latest/policy/CMP0074.html

     

    CMP0074 — CMake 3.29.0-rc4 Documentation

    CMP0074 find_package() uses _ROOT variables. In CMake 3.12 and above the find_package( ) command now searches prefixes specified by the _ROOT CMake variable and the _ROOT environment variable. Package roots are maintained as a stack so nested calls to all

    cmake.org

     

     

    booststrap을 클릭하면 여러 파일이 생기는 데, 이중 project-config.jam을 수정해주어야 한다.

    파이썬 버전과 경로는 본인에게 맞춰서 변경할 것

     

    # Boost.Build Configuration 
    # Automatically generated by bootstrap.bat 
     
    import option ; 
     
    using msvc ; 
    
    using python : 3.11 : "C:\\Program Files\\Python311" : : : <address-model>64 ;
     
    option.set keep-going : false ;

    파이썬 버전과 경로를 추가해주면 된다. 설정이 안되어있으면, 파이썬 라이브러리가 빌드되지 않으며, 반드시 역슬레쉬(\) 또는 원화 기호를 2번 써주어야 한다. 경로 복붙할 때 주의할 것. 기본적으로 정적 링크용만 생성되며, 동적 링크용 라이브러리까지 생성시키고 싶다면,  --build-type=complete 옵션을 주어야 한다.

    #전체 라이브러리 빌드 (오래 걸림)
    b2 --toolset=msvc-14.3 --build-type=complete architecture=x86 address-model=64 stage
    
    #--with-<library>를 이용해 파이썬 라이브러리만 빌드
    b2 --toolset=msvc-14.3 --with-python --build-type=complete architecture=x86 address-model=64 stage

    msvc 버전은 v140이면 14.0, v142면 14.2이렇게 하면 된다. (참고로 2019 버전은 v142이며, 2022 버전은 v143이다.  Visual Studio Installer에서 확인해볼 수 있다.) 추가적으로 64비트 라이브러리를 빌드하기 위해 architecture=x86 address-model=64 옵션을 주어 명시하였다. (참고로 --with-<library> 또는 --without-<library> 옵션이 없다면 전체를 빌드한다.)

    boost_python311 이렇게 되어있는게 동적 링크 버전의 lib이고

    libboost_python311 이렇게 되어있는게 정적 링크 버전의 lib이다.

    https://pknam.tistory.com/12

     

    boost::python Embedding

    C++ 프로그램에 Python embedding을 해보았다. Python을 설치하면 생기는 헤더/라이브러리 파일만을 이용해 개발할 수도 있지만 C++가 아닌 C로 개발해야 해서 생산성이 떨어진다boost::python은 그것을 클

    pknam.tistory.com

    cmake -B build -G Ninja -D Boost_ROOT=../boost_1_84_0

     

    Boost.Python 정적 링크

    공식 문서를 참고해보면 정적 링크 버전의 라이브러리를 사용하기 위해서는 Boost_USE_STATIC_LIBS=ON으로 설정해주어야 한다.

    cmake -B build -G Ninja -D Boost_ROOT=../boost_1_84_0 -D Boost_USE_STATIC_LIBS=ON

    https://cmake.org/cmake/help/latest/module/FindBoost.html

     

    FindBoost — CMake 3.29.0-rc3 Documentation

    FindBoost Find Boost include dirs and libraries Use this module by invoking find_package() with the form: find_package(Boost [version] [EXACT] # Minimum or EXACT version e.g. 1.67.0 [REQUIRED] # Fail with error if Boost is not found [COMPONENTS ...] # Boos

    cmake.org

    #define BOOST_PYTHON_STATIC_LIB
    #include <boost/python.hpp>

    또한 boost/python.hpp를 include하기 전, 위와 같이 선언해주어야 한다.

    출력, 정적 링크

    개인적으로 코드를 짤때는 컴파일 시간이 절약되도록 동적 링크를 사용하고, 배포할 땐, dll 이름이 깔끔하진 않으니 정적 링크하면 좋을 것 같다.

     

    추가 boost msvc 버전 설정하기

    Qt와 같은 라이브러리를 사용하는 경우 Visual Studio 2019 (v142)를 사용하여 Boost Library를 빌드하여야 Qt 콘솔창에서 cmake를 실행하였을 때 boost 라이브러리를 찾을 수 있다. 2022 commuity와 2019 buildtool이 설치되어있는 컴퓨터에서 bootstrap을 실행하였을 때 2022로 설정이되어버려 cmake에서 빌드한 boost 라이브러리를 못찾는 것을 경험하였으며, 아래와 같이 project-config.jam에서 msvc의 버전을 지정하는 것으로 해결하였다.

    b2 --with-python --toolset-msvc=14.2 architecture=x86 address-model=64 --build-type=complete

     

    pybind11

    pybind11에서도 비슷하게 할 수 있는 것으로 보인다. 공식 문서에서는 인터프리터를 재시작할 때 몇가지 주의사항이 있을 수 있으며, pybind11로 만든 모듈이 아닌 third-party 모듈일 경우 재시작때 메모리가 셀 수도 있다고 공식 문서에 적혀있다.

    https://pybind11.readthedocs.io/en/stable/advanced/embedding.html

     

    Embedding the interpreter - pybind11 documentation

    Previous Utilities

    pybind11.readthedocs.io

     

    추가) Exception Handling

    API 문서에 따르면 내장된 파이썬에서 오류가 발생하였을 때, PyErr_Print()를 호출하면 에러메세지를 stderr에 출력한다.

    https://docs.python.org/3/c-api/exceptions.html

     

    Exception Handling

    The functions described in this chapter will let you handle and raise Python exceptions. It is important to understand some of the basics of Python exception handling. It works somewhat like the PO...

    docs.python.org

     

    다만 에러가 안생겼을 때 이를 호출하면 오류가 발생하므로 PyErr_Ocurred()를 호출하여 에러가 발생했는 지 확인한 후 PyErr_Print()를 호출하는 것을 추천한다.

    위의 두 함수가 편리하긴 하지만, GUI를 사용하는 경우에는 콘솔창의 에러메세지를 확인할 수 없다는 문제가 있다. 그래서 stderr에 출력하는 것이 아니라 문자열을 받아와서 MessageBox로 띄우거나 로그 파일을 작성할 필요가 있다. (API 문서나 구글에서 찾아봐도 이에 대한 자료가 부족하다.)

     

    API문서를 찾아보면 Python 3.12부터 PyErr_GetRaisedException() 함수를 통해 python exception 객체를 직접 얻을 수 있다.

    3.12 미만의 버전에는 PyErr_Fetch를 사용하니 참고

     

    여기서 Python 인터프리터 상에서 Exception 객체를 살펴보자

    import traceback
    try:
        print(a)
    except Exception as exc:
        print(exc.__class__)
        print(exc.__cause__)
        print(exc.__context__)
        print(exc.__str__())
        print(exc.args)
        
        print(traceback.format_exception(exc))

    __cause__와 __context__가 None이라는게 의외이다.

    exception 객체를 받은 뒤, taceback 모듈의 format_excpetion()함수를 호출하면, 출력할 문자열 리스트를 얻을 수 있다. 그리고 C API 상에서는 if으로 반환값을 확인하여 에러가 일어났는지 확인할 수 밖에 없지만, boost python을 사용한다면 에러가 발생하였을때 error_already_set을 throw해준다. (에러 내용까지 포함해주었으면 좋겠는데 생성자와 소멸자밖에 없는 객체라는 것이 조금 아쉽다.)

     

    이를 이용하면 c++에서는 다음과 같이 exception을 처리할 수 있다.

    #include <iostream>
    #include <sstream>
    
    #include <windows.h>
    
    #include "boost/python.hpp"
    namespace bp = boost::python;
    
    int main(int argc, char* argv[]) {
        try{
            //code
        }
        catch(bp::error_already_set&) {
            bp::object exc = bp::object(bp::handle(PyErr_GetRaisedException()));
            bp::object traceback = bp::import("traceback");
            bp::object format_exception = traceback.attr("format_exception");
            bp::list msgList = bp::list(format_exception(exc));
            
            std::stringstream ss;
            for(int i=0; i < len(msgList); ++i) ss << bp::extract<char*>(msgList[i]);
    
            std::cerr << ss.str() << std::endl;
            MessageBoxExA(NULL, ss.str().c_str(), "Python Error", MB_OK, 0);
            return 1;
        }
    
    }

     

    2024.03.05 Boost.Python 내용 추가

    2024.03.10 Boost Build 관련 내용 추가, __main__ 모듈 설명 추가

    2024.03.17 Cmake CMP0074 policy, boost msvc 버전 지정 추가, configure_file 참고 추가

    2024.07.08 Excpetion Handling 추가

    댓글

Designed by Tistory.