-
[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 함수를 통해 파이썬 코드를 실행할 필요가 있으며, 공통적인 흐름은 다음과 같다.
- 모듈 (.py 파일) import -> PyImport_Import(PyObject* name) or PyImport_ImportModule(const char* name)
- 해당 모듈에 정의된 Attribute 레퍼런스 얻기 -> PyObject_GetAttr(PyObject* o, PyObject* name) or PyObject_GetAttrString(PyObject* o, const char* name)
- PyObject_Call, PyObject_CallObject, PyList_SetItem과 같은 함수로 객체 사용
- 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.zip0.01MBBuild 과정
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이다.
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 추가
'기타 코딩' 카테고리의 다른 글
[C++/Win API] 계산기 만들기 - WinAPI와 GUI편 (0) 2024.07.27 [C++/Win API] 계산기 만들기-핵심 알고리즘 편 (0) 2024.07.27 [Python] 파이썬 코드 배포하기 (0) 2024.03.01