Calling Python Functions From C++ Using Cython
Calling Python Functions From C++ Using Cython
In this post, I’ll quickly show you how to set up a simple C++ project using CMake. This setup allows you to call Python functions from within a C++ program using Cython. Instead of diving deep into explanations, I’ll just provide the code for you to refer to.
Cython is a superset of the programming language Python, which allows developers to write Python code that yields performance comparable to that of C. Cython is a compiled language that is typically used to generate CPython extension modules.
Let’s get started.
The Directory Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyApp/
| cmake/
| | FindCython.cmake
| PythonPlugins/
| | hello.py
| src/
| | PythonFunctions/
| | | CMakeLists.txt
| | | Playground.pyx
| | CMakeLists.txt
| | main.cpp
| | MyAppConfig.in
| CMakeLists.txt
| make-me
FindCython.cmake
MyApp/cmake/FindCython.cmake
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Use the Cython executable that lives next to the Python executable
# if it is a local installation.
find_package(Python3 REQUIRED)
if( Python3_FOUND )
get_filename_component( _python_path ${Python3_EXECUTABLE} PATH )
find_program( CYTHON_EXECUTABLE
NAMES cython cython.bat cython3
HINTS ${_python_path}
)
else()
find_program( CYTHON_EXECUTABLE
NAMES cython cython.bat cython3
)
endif()
include( FindPackageHandleStandardArgs )
FIND_PACKAGE_HANDLE_STANDARD_ARGS( Cython REQUIRED_VARS CYTHON_EXECUTABLE )
mark_as_advanced( CYTHON_EXECUTABLE )
Top-level CMakeLists.txt
MyApp/CMakeLists.txt
:
1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.10)
project(MyApp VERSION 1.0)
#########################################################
# CMake Modules
#########################################################
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_LIST_DIR}/cmake)
add_subdirectory(src)
MyApp/make-me
:
1
2
3
4
5
#!/bin/bash
cmake -E make_directory "build"
cd build
cmake ..
make
Main
MyApp/src/CMakeLists.txt
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#########################################################
# C++ Settings
#########################################################
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_BUILD_TYPE debug)
#########################################################
# Configure a header file to pass some of
# the CMake settings to the source code
#########################################################
configure_file(${PROJECT_NAME}Config.in ${PROJECT_NAME}Config.h)
#########################################################
# Main Executable
#########################################################
add_executable(${PROJECT_NAME} "main.cpp")
#########################################################
# PythonFunctions
#########################################################
add_subdirectory(PythonFunctions)
target_link_libraries(${PROJECT_NAME} PUBLIC PythonFunctions)
#########################################################
# Add the binary tree to the search path for include
# files so that we will find MyAppConfig.h
#########################################################
target_include_directories(${PROJECT_NAME} PUBLIC "${PROJECT_BINARY_DIR}/src")
MyAppConfig.in
1
2
3
// the configured options and settings for MyApp
#define MyApp_VERSION_MAJOR @MyApp_VERSION_MAJOR@
#define MyApp_VERSION_MINOR @MyApp_VERSION_MINOR@
PythonFunctions
MyApp/src/PythonFunctions/CMakeLists.txt
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
set(PYTHON_APP_NAME "Playground")
find_package(Cython REQUIRED)
find_package(Python3 COMPONENTS Interpreter Development REQUIRED)
file(COPY ${PYTHON_APP_NAME}.pyx DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
add_custom_command(
OUTPUT ${PYTHON_APP_NAME}.c ${PYTHON_APP_NAME}.h
COMMAND ${CYTHON_EXECUTABLE} ${PYTHON_APP_NAME}.pyx
DEPENDS ${PYTHON_APP_NAME}.pyx)
set(SOURCES ${PYTHON_APP_NAME}.c)
Python3_add_library(${PYTHON_APP_NAME} STATIC ${SOURCES})
add_library(PythonFunctions ${PYTHON_APP_NAME}.c)
target_include_directories(PythonFunctions
PUBLIC
$<INSTALL_INTERFACE:${PYTHON_APP_NAME}.h>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
PRIVATE
${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(PythonFunctions PUBLIC ${PYTHON_APP_NAME} ${Python3_INCLUDE_DIRS})
target_link_libraries(PythonFunctions PRIVATE ${PYTHON_APP_NAME} ${Python3_LIBRARIES})
MyApp/src/PythonFunctions/Playground.pyx
1
2
3
4
5
6
7
8
9
10
11
# cython: language_level=3
import sys
import os
dir_path = os.path.dirname(os.path.realpath(__file__))
sys.path.append(f"{dir_path}/PythonPlugins") # Update list of directories that the Python interpreter will search for when it tries to resolve a module name
from hello import hello_world
cdef public char* call_hello_world(char* message, list names):
res = hello_world(message.decode('UTF-8'), names)
return res.encode('UTF-8')
main.cpp
MyApp/src/main.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>
#include <Python.h>
#include "MyAppConfig.h"
#include "Playground.h"
int main(int argc, char *argv[])
{
std::cout << argv[0]
<< " Version "
<< MyApp_VERSION_MAJOR
<< "."
<< MyApp_VERSION_MINOR
<< std::endl;
PyImport_AppendInittab("Playground", PyInit_Playground);
Py_Initialize();
PyImport_ImportModule("Playground");
PyObject *pNames;
if (!(pNames = PyList_New(0)))
{
return 1;
}
for (int i = 1; i <= 3; i++)
{
std::string n = "Name_" + std::to_string(i);
PyList_Append(pNames, PyUnicode_FromString(n.c_str()));
}
char *pResult = call_hello_world("Hello", pNames);
std::cout << "Result from Python call: " << pResult << std::endl;
Py_Finalize();
return 0;
}
PythonPlugins
MyApp/PythonPlugins/hello.py
1
2
3
4
5
6
7
from typing import List
def hello_world(text: str, names: List[str]) -> str:
names_joined = ",".join(names)
message = f"{text} {names_joined}!"
print(f"In Python: {message}")
return message+"done"
Build and Run (Visual Studio Code)
.vscode/launch.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"version": "0.2.0",
"configurations": [
{
"name": "Build and Run",
"type": "lldb",
"request": "launch",
"program": "${workspaceRoot}/build/src/myapp",
"args": [],
"cwd": "${workspaceFolder}",
"preLaunchTask": "build"
}
]
}
.vscode/tasks.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "build",
"command": "./make-me",
"args": [],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": {
"base": "$gcc",
"fileLocation": ["relative", "${workspaceFolder}"]
},
"group": {
"kind": "build",
"isDefault": true
},
"detail": "make-me => Makefile => g++"
}
]
}
Running
1
2
3
4
5
6
$ ./make-me
$ ./build/src/MyApp
./build/src/MyApp Version 1.0
In Python: Hello Name_1,Name_2,Name_3!
Result from Python call: Hello Name_1,Name_2,Name_3!done