Tutorial: Binding C++ Functions and Classes to Python using Cython
Table of Contents
Introduction
Project Structure
C++ Implementation
Cython Binding Files
Building the Extension
Using the Python Module
Best Practices
Introduction Cython is a powerful tool that allows you to write C extensions for Python. It’s particularly useful when you want to:
Speed up Python code by converting it to C
Interface with existing C/C++ code
Create Python bindings for C++ classes and functions
This tutorial demonstrates how to create Python bindings for C++ functions and classes using a real-world example.
Project Structure A typical Cython project for C++ bindings consists of the following files:
1 2 3 4 5 6 src/cc/ ├── functions.hpp # C++ header file ├── functions.cc # C++ implementation ├── functions.pxd # Cython declarations ├── functions.pyx # Cython implementation └── setup.py # Build configuration
You can find the code 🔗here .
C++ Implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #ifndef CC_FUNCTIONS_HPP #define CC_FUNCTIONS_HPP #include <vector> std::vector<int > prim (int n) ;std::vector<int > fib (int n) ;std::vector<std::vector<int >> matmul (std::vector<std::vector<int >> &A, std::vector<std::vector<int >> &B); class matrix {public : matrix (); std::vector<std::vector<int >> mul (std::vector<std::vector<int >> &A, std::vector<std::vector<int >> &B); std::vector<std::vector<int >> transpose (std::vector<std::vector<int >> &A); }; #endif
Implementation File(functions.cc) 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include "functions.hpp" #include <stdexcept> std::vector<int > prim (int n) { if (n <= 0 ) return {}; if (n == 1 ) return {2 }; int upper = static_cast <int >(n * (log (n) + log (log (n)))) + 10 ; std::vector<bool > sieve (upper + 1 , true ) ; sieve[0 ] = sieve[1 ] = false ; for (int p = 2 ; p * p <= upper; p++) { if (sieve[p]) { for (int i = p * p; i <= upper; i += p) { sieve[i] = false ; } } } std::vector<int > primes; primes.reserve (n); for (int i = 2 ; primes.size () < static_cast <size_t >(n) && i <= upper; i++) { if (sieve[i]) { primes.push_back (i); } } return primes; } std::vector<int > fib (int n) { std::vector<int > res (n) ; res[0 ] = 1 ; res[1 ] = 1 ; for (int i = 2 ; i < n; ++i) { res[i] = res[i - 1 ] + res[i - 2 ]; } return res; } std::vector<std::vector<int >> matmul (std::vector<std::vector<int >> &A, std::vector<std::vector<int >> &B) { if (A.empty () || B.empty () || A[0 ].size () != B.size ()) { throw std::invalid_argument ( "Invalid matrix dimensions for multiplication." ); } std::vector<std::vector<int >> res (A.size (), std::vector <int >(B[0 ].size (), 0 )); for (int i = 0 ; i < A.size (); ++i) { for (int j = 0 ; j < B[0 ].size (); ++j) { for (int k = 0 ; k < B.size (); ++k) { res[i][j] += A[i][k] * B[k][j]; } } } return res; } std::vector<std::vector<int >> matrix::mul (std::vector<std::vector<int >> &A, std::vector<std::vector<int >> &B) { if (A.empty () || B.empty () || A[0 ].size () != B.size ()) { throw std::invalid_argument ( "Invalid matrix dimensions for multiplication." ); } std::vector<std::vector<int >> res (A.size (), std::vector <int >(B[0 ].size (), 0 )); for (int i = 0 ; i < A.size (); ++i) { for (int j = 0 ; j < B[0 ].size (); ++j) { for (int k = 0 ; k < B.size (); ++k) { res[i][j] += A[i][k] * B[k][j]; } } } return res; } matrix::matrix () {} std::vector<std::vector<int >> matrix::transpose (std::vector<std::vector<int >> &A) { if (A.empty ()) { throw std::invalid_argument ("Invalid matrix dimensions for transpose." ); } int row = A.size (); int col = A[0 ].size (); std::vector<std::vector<int >> res (col, std::vector <int >(row, 0 )); for (int i = 0 ; i < row; ++i) { for (int j = 0 ; j < col; ++j) { res[j][i] = A[i][j]; } } return res; }
Cython Binding Files 1. PXD File (functions.pxd) The PXD file contains declarations that tell Cython about the C++ types and functions:
1 2 3 4 5 6 7 8 9 10 11 12 13 from libcpp.vector cimport vectorcdef extern from "functions.cc" : cdef vector[int ] prim(int n) cdef vector[int ] fib(int n) cdef vector[vector[int ]] matmul(vector[vector[int ]] A, vector[vector[int ]] B) cdef cppclass matrix: matrix() vector[vector[int ]] mul(vector[vector[int ]] A, vector[vector[int ]] B) vector[vector[int ]] transpose(vector[vector[int ]] A)
❌ Intuitively, it should be cdef extern from "functions.hpp"
in functions.pxd
, and the TVM project also includes header files (.hpp
) in its .pxi
. However, this causes errors on my Windows computer. Changing it to .cc
resolves the issue.
2. PYX File (functions.pyx) The PYX file contains the Python interface implementation:
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 37 38 39 40 41 from libcpp.vector cimport vectorfrom functions cimport prim, fib, matmul, matrixdef getNPrimes (int n ): """ Get the first n prime numbers. """ cdef vector[int ] primes = prim(n) return primes def getNFibonacci (int n ): """ Get the first n Fibonacci numbers. """ cdef vector[int ] res = fib(n) return res def matMul (vector[vector[int ]] A, vector[vector[int ]] B ): """ Multiply two matrices. """ cdef vector[vector[int ]] res = matmul(A, B) return res cdef class PyMatrix : cdef matrix _matrix def __init__ (self ): self._matrix = matrix() def mul (self, vector[vector[int ]] A, vector[vector[int ]] B ): return self._matrix.mul(A, B) def transpose (self, vector[vector[int ]] A ): return self._matrix.transpose(A)
Building the Extension Create a setup.py
file to build the Cython extension:
1 2 3 4 5 6 from setuptools import setupfrom Cython.Build import cythonizesetup( ext_modules=cythonize("functions.pyx" ) )
To build the extension, run:
1 python setup.py build_ext --inplace
The command above will generate a .so
file(.pyd
in Windows), for example, functions.so
.
Using the Python Module After building, you can use the module in Python:
1 2 3 4 5 6 7 8 9 10 11 12 13 from functions import getNPrimes, getNFibonacci, matMul, PyMatrixprimes = getNPrimes(10 ) fibonacci = getNFibonacci(10 ) matrix = PyMatrix() A = [[1 , 2 ], [3 , 4 ]] B = [[5 , 6 ], [7 , 8 ]] result = matrix.mul(A, B) transposed = matrix.transpose(A)