Post

Calling Python Functions From C++ Using Cython

Calling Python Functions From C++ Using Cython

GitHub repository

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
This post is licensed under CC BY 4.0 by the author.