Skip to content
This repository was archived by the owner on Oct 20, 2022. It is now read-only.

Commit 717fa9f

Browse files
author
Matus Novak
committed
Initial release
0 parents  commit 717fa9f

File tree

8 files changed

+345
-0
lines changed

8 files changed

+345
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
build/
2+
.vs
3+
.vscode
4+
*.tmp
5+
*.suo
6+
*.TMP
7+
*.log

.gitmodules

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[submodule "libs/cpython"]
2+
path = libs/cpython
3+
url = https://github.com/python/cpython.git
4+
branch = 491bbedc209fea314a04cb3015da68fb0aa63238
5+
[submodule "libs/pybind11"]
6+
path = libs/pybind11
7+
url = https://github.com/pybind/pybind11.git
8+
branch = master

CMakeLists.txt

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
cmake_minimum_required(VERSION 3.1)
2+
include(${CMAKE_ROOT}/Modules/ExternalProject.cmake)
3+
project (PythonEmbeddedExample)
4+
5+
# Check for C++11
6+
set (CMAKE_CXX_STANDARD 11)
7+
8+
# Specify build type
9+
set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Select build type")
10+
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "RelWithDebInfo" "MinSizeRel")
11+
12+
if (CMAKE_BUILD_TYPE MATCHES "Debug")
13+
set(CPYTHON_BUILD_TYPE Debug)
14+
else ()
15+
set(CPYTHON_BUILD_TYPE Release)
16+
endif()
17+
18+
# Add the cpython as an external project that will be included in the build
19+
if(MSVC)
20+
if(CMAKE_CL_64)
21+
set(CPYTHON_PLATFORM x64)
22+
set(CPYTHON_BUILD_DIR amd64)
23+
else()
24+
set(CPYTHON_PLATFORM x86)
25+
set(CPYTHON_BUILD_DIR win32)
26+
endif()
27+
ExternalProject_Add(CPYTHON
28+
DOWNLOAD_COMMAND ""
29+
SOURCE_DIR ${CMAKE_SOURCE_DIR}/libs/cpython
30+
CONFIGURE_COMMAND ""
31+
BUILD_COMMAND cd ${CMAKE_SOURCE_DIR}/libs/cpython && MSBuild.exe /p:Configuration=${CPYTHON_BUILD_TYPE} /property:Platform=${CPYTHON_PLATFORM} "PCBuild/python.vcxproj" /nologo /verbosity:minimal /consoleloggerparameters:summar
32+
INSTALL_COMMAND ""
33+
TEST_COMMAND ""
34+
)
35+
else()
36+
ExternalProject_Add(CPYTHON
37+
DOWNLOAD_COMMAND ""
38+
SOURCE_DIR ${CMAKE_SOURCE_DIR}/libs/cpython
39+
CONFIGURE_COMMAND cd ${CMAKE_SOURCE_DIR}/libs/cpython && ./configure --disable-static --enable-shared
40+
BUILD_COMMAND cd ${CMAKE_SOURCE_DIR}/libs/cpython && make
41+
INSTALL_COMMAND ""
42+
TEST_COMMAND ""
43+
)
44+
endif()
45+
46+
set(CPYTHON_STDLIB_DIR ${CMAKE_SOURCE_DIR}/libs/cpython/Lib)
47+
if(MSVC)
48+
set(CPYTHON_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/cpython/Include ${CMAKE_SOURCE_DIR}/libs/cpython/PC)
49+
set(CPYTHON_LIBRARY_DIR ${CMAKE_SOURCE_DIR}/libs/cpython/PCBuild/${CPYTHON_BUILD_DIR})
50+
if(CMAKE_BUILD_TYPE MATCHES "Debug")
51+
set(CPYTHON_BIN ${CMAKE_SOURCE_DIR}/libs/cpython/PCBuild/${CPYTHON_BUILD_DIR}/python38_d.dll)
52+
else()
53+
set(CPYTHON_BIN ${CMAKE_SOURCE_DIR}/libs/cpython/PCBuild/${CPYTHON_BUILD_DIR}/python38.dll)
54+
endif()
55+
else()
56+
set(CPYTHON_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/cpython/Include ${CMAKE_SOURCE_DIR}/libs/cpython)
57+
set(CPYTHON_LIBRARY_DIR ${CMAKE_SOURCE_DIR}/libs/cpython)
58+
set(CPYTHON_LIBRARY python3.8m)
59+
set(CPYTHON_BIN ${CMAKE_SOURCE_DIR}/libs/cpython/libpython3.8m.so)
60+
endif()
61+
62+
# Add the pybind11 library (optional)
63+
ExternalProject_Add(PYBIND
64+
DOWNLOAD_COMMAND ""
65+
SOURCE_DIR ${CMAKE_SOURCE_DIR}/libs/pybind11
66+
CMAKE_ARGS -DPYBIND11_TEST=OFF -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DPYTHONLIBS_FOUND=ON -DPYTHON_MODULE_EXTENSION=.lib -DPYTHON_LIBRARY=${CMAKE_SOURCE_DIR}/libs/cpython/PCBuild/win32/python38_d.lib -DPYTHON_INCLUDE_DIR=${CMAKE_SOURCE_DIR}/libs/cpython/PCBuild/win32
67+
BUILD_COMMAND cmake --build . --config ${CMAKE_BUILD_TYPE}
68+
INSTALL_COMMAND ""
69+
TEST_COMMAND ""
70+
)
71+
72+
set(PYBIND_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/libs/pybind11/include)
73+
74+
# Source and header files
75+
FILE(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp)
76+
FILE(GLOB_RECURSE HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/*.h)
77+
78+
# Linking directories
79+
link_directories(${CPYTHON_LIBRARY_DIR})
80+
81+
# The target executable
82+
add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS})
83+
84+
# Add macros
85+
target_compile_definitions(${PROJECT_NAME} PRIVATE NOMINMAX=1)
86+
87+
# Dependencies
88+
add_dependencies(${PROJECT_NAME} PYBIND)
89+
add_dependencies(${PROJECT_NAME} CPYTHON)
90+
91+
# Include directories
92+
target_include_directories(${PROJECT_NAME} PRIVATE ${CPYTHON_INCLUDE_DIR})
93+
target_include_directories(${PROJECT_NAME} PRIVATE ${PYBIND_INCLUDE_DIR})
94+
95+
# On MSVC build, the python library is automatically linked (crazy I know)
96+
if(NOT MSVC)
97+
target_link_libraries(${PROJECT_NAME} ${CPYTHON_LIBRARY})
98+
endif()
99+
100+
# Set the executable to console application if MSVC
101+
if(MSVC)
102+
set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "/SUBSYSTEM:CONSOLE")
103+
endif()
104+
105+
# Copy Python DLL to the build folder if different
106+
add_custom_command(
107+
TARGET ${PROJECT_NAME}
108+
POST_BUILD
109+
COMMAND ${CMAKE_COMMAND}
110+
-E copy_if_different ${CPYTHON_BIN} $<TARGET_FILE_DIR:${PROJECT_NAME}>
111+
)
112+
113+
# Copy our python sources to the build folder
114+
add_custom_command(
115+
TARGET ${PROJECT_NAME}
116+
POST_BUILD
117+
COMMAND ${CMAKE_COMMAND}
118+
-E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/src/app $<TARGET_FILE_DIR:${PROJECT_NAME}>/app
119+
)
120+
121+
# Copy the Python stdlib into the build folder (needed by the embedded python)
122+
add_custom_command(
123+
TARGET ${PROJECT_NAME}
124+
POST_BUILD
125+
COMMAND ${CMAKE_COMMAND}
126+
-E copy_directory ${CPYTHON_STDLIB_DIR} $<TARGET_FILE_DIR:${PROJECT_NAME}>/lib
127+
)

README.md

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Python Embedded Example Project
2+
3+
This is an example project using mebedded python 3.8 (cpython) in C++ console application using CMake. This example project also contains pybind11 library for easy binding between C++ and python.
4+
5+
Tested on Windows 10 with Visual Studio 2013, 2015, and 2017 (both x86 and x64). Also tested on Ubuntu with GCC (x64).
6+
7+
**Note this example has nothing to do with embedding C++ in python! This is the other way around! Embeding python in C++.**
8+
9+
## About
10+
11+
Normally, you have a python installed in your system and then you can launch python scripts from command line. What if you want to create C++ application, but want to use python scripts (for example as modding for games)? One option is to let the user install python on their system and then install your app. No problem there, except, you are forcing the user to modify their system. Additonally, you can never guarantee that the user will install the specific python version (2.7 or 3.8) you require. Even if all of that is sorted out, the python that will get run will probably need third party packages (or you need to explicitly disable some packages), this can't be done easily.
12+
13+
What if we can embed entire python in C++ executable? This way, the user won't have to install python on their system nor additional dependencies. Everything will be bundled up in the executable.
14+
15+
This project comes with cpython 3.8 (as a git submodule) and pybind11. Once you build the project, your build folder will look like this:
16+
17+
```
18+
python-embedded-example-project/
19+
build/
20+
Release/
21+
PythonEmbeddedExample.exe
22+
app/
23+
example.py
24+
lib/
25+
a lot of Python standard library files
26+
```
27+
28+
The script files (such as example.py) are copied to the build directory from `src/app` and the python standard libraries are copied over from git submodule located in `python-embedded-example-project/libs/cpython/Lib`. You can't get rid of those standard libraries! Python needs them to initialise. However, you could limit the standard libraries by only selectively copying over the ones you need.
29+
30+
When the executable runs, it will load an `example` module and creates an instance of `Example` class from `example.py` file. That's all it does.
31+
32+
**The python PATH is limited only to the app and lib folder located next to the executable.** The embedded python won't have any access to installed packages in your operating system. You will have to include them in any of those two folders.
33+
34+
## Requirements
35+
36+
Visual Studio 2013 (or newer) or Linux with GCC. MinGW is sadly not supported, and clang has not been tested.
37+
38+
## Download
39+
40+
Don't forget to initialise and update the submodules!
41+
42+
```
43+
git clone https://github.com/matusnovak/python-embedded-example-project
44+
cd python-embedded-example-project
45+
git submodile init
46+
git submodule update
47+
```
48+
49+
## Build using Visual Studio on Windows
50+
51+
**Please note:** that you have to explicitly specify `CMAKE_BUILD_TYPE`. If you specify Debug, then you must use Debug in your Visual Studio. You won't be able to change to Release using the dropdown list in the main menu. Python will be build using the `CMAKE_BUILD_TYPE` flag regarding the chosen configuration in Visual Studio. To change from Debug to Release, re-run the cmake and set the `CMAKE_BUILD_TYPE` to Release.
52+
53+
```
54+
cd python-embedded-example-project
55+
mkdir build
56+
cd build
57+
cmake .. -G "Visual Studio 15 2017" -DCMAKE_BUILD_TYPE=Debug
58+
```
59+
60+
Then open the generated solution file "PythonEmbeddedExample.sln" in the build folder. You should see the following projects:
61+
62+
* **ALL_BUILD** - Building this will build all projects
63+
* **CPYTHON** - The embedded python project
64+
* **PYBIND** - The pybind11 library for binding
65+
* **PythonEmbeddedExample** - This is the example project
66+
67+
Build the PythonEmbeddedExample and then run the `PythonEmbeddedExample.exe` from command line. That's all.
68+
69+
## Build using GCC on Linux
70+
71+
```
72+
cd python-embedded-example-project
73+
mkdir build
74+
cd build
75+
cmake .. -DCMAKE_BUILD_TYPE=Debug
76+
```
77+
78+
Then build the example by running:
79+
80+
```
81+
make
82+
```
83+
84+
The `PythonEmbeddedExample` executable will be generated.
85+
86+
## Example output
87+
88+
Example output of the `PythonEmbeddedExample` executable.
89+
90+
```
91+
Python PATH set to: C:\Users\matus\Documents\cpp\python-embedded-example-project\build\Debug\lib;C:\Users\matus\Documents\cpp\python-embedded-example-project\build\Debug\app;
92+
Importing module...Initializing class...
93+
Example constructor with msg: Hello World
94+
Got msg back on C++ side: Hello World
95+
```

libs/cpython

Submodule cpython added at 491bbed

libs/pybind11

Submodule pybind11 added at f5f6618

src/app/example.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Example:
2+
def __init__(self, msg: str):
3+
self.msg = msg
4+
print('Example constructor with msg:', self.msg)
5+
6+
def getMsg(self):
7+
return self.msg

src/main.cpp

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#if defined(WIN32) || defined(_WIN32) || defined(__WIN32) && !defined(__CYGWIN__)
2+
#define IS_WINDOWS
3+
#include <Windows.h> // Needed by GetModuleFileNameW
4+
#else
5+
#include <libgen.h> // Needed by readlink
6+
#endif
7+
8+
#include <iostream>
9+
#include <Python.h>
10+
#ifndef IS_WINDOWS
11+
#pragma GCC diagnostic push
12+
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
13+
#endif
14+
#include <pybind11/pybind11.h>
15+
#include <pybind11/eval.h>
16+
#include <pybind11/embed.h>
17+
#ifndef IS_WINDOWS
18+
#pragma GCC diagnostic pop
19+
#endif
20+
21+
namespace py = pybind11;
22+
using namespace py::literals;
23+
24+
///=============================================================================
25+
#ifdef IS_WINDOWS
26+
std::wstring getExecutableDir() {
27+
wchar_t exePath[MAX_PATH];
28+
GetModuleFileNameW(nullptr, exePath, MAX_PATH);
29+
const auto executableDir = std::wstring(exePath);
30+
const auto pos = executableDir.find_last_of('\\');
31+
if (pos != std::string::npos) return executableDir.substr(0, pos);
32+
return L"\\";
33+
}
34+
#else
35+
std::wstring getExecutableDir() {
36+
char result[PATH_MAX];
37+
ssize_t count = readlink("/proc/self/exe", result, PATH_MAX);
38+
if (count != -1) {
39+
const auto path = std::string(dirname(result));
40+
return std::wstring(path.begin(), path.end());
41+
}
42+
return L"/";
43+
}
44+
#endif
45+
46+
///=============================================================================
47+
int main(int argc, char** argv) {
48+
(void)argc;
49+
(void)argv;
50+
51+
// Get executable dir and build python PATH variable
52+
const auto exeDir = getExecutableDir();
53+
#ifdef IS_WINDOWS
54+
const auto pythonHome = exeDir + L"\\lib";
55+
const auto pythonPath = exeDir + L"\\lib;" + exeDir + L"\\app;";
56+
#else
57+
const auto pythonHome = exeDir + L"/lib";
58+
const auto pythonPath = exeDir + L"/lib:" + exeDir + L"/app";
59+
#endif
60+
61+
// Initialize python
62+
Py_OptimizeFlag = 1;
63+
Py_SetProgramName(L"PythonEmbeddedExample");
64+
Py_SetPath(pythonPath.c_str());
65+
Py_SetPythonHome(pythonHome.c_str());
66+
67+
std::wcout << "Python PATH set to: " << pythonPath << std::endl;
68+
69+
try {
70+
py::scoped_interpreter guard{};
71+
72+
// Disable build of __pycache__ folders
73+
py::exec(R"(
74+
import sys
75+
sys.dont_write_bytecode = True
76+
)");
77+
78+
// This imports example.py from app/example.py
79+
// The app folder is the root folder so you don't need to specify app.example.
80+
// The app/example script that is being imported is from the actual build folder!
81+
// Cmake will copy the python scripts after you have compiled the source code.
82+
std::cout << "Importing module..." << std::endl;
83+
auto example = py::module::import("example");
84+
85+
std::cout << "Initializing class..." << std::endl;
86+
const auto myExampleClass = example.attr("Example");
87+
auto myExampleInstance = myExampleClass("Hello World"); // Calls the constructor
88+
// Will print in the terminal window:
89+
// Example constructor with msg: Hello World
90+
91+
const auto msg = myExampleInstance.attr("getMsg")(); // Calls the getMsg
92+
std::cout << "Got msg back on C++ side: " << msg.cast<std::string>() << std::endl;
93+
} catch (std::exception& e) {
94+
std::cerr << "Something went wrong: " << e.what() << std::endl;
95+
return EXIT_FAILURE;
96+
}
97+
98+
return EXIT_SUCCESS;
99+
}

0 commit comments

Comments
 (0)