Interfacing with Python and C/C++ Libraries¶
This guide covers advanced techniques for integrating Cython with Python code, C libraries, C++ libraries, and managing the Global Interpreter Lock (GIL).
Python and Cython Integration¶
Function Visibility Levels¶
Cython provides three function declaration types:
# Python-only function (slow, full Python semantics)
def python_func(x):
return x * 2
# C-only function (fast, no Python overhead)
cdef int c_func(int x):
return x * 2
# Hybrid function (accessible from Python and C)
cpdef int hybrid_func(int x):
return x * 2
Performance comparison:
- cdef: Fastest, no Python overhead, C-only
- cpdef: Fast from C, slight overhead from Python
- def: Standard Python speed
Calling Conventions¶
# From Python
import mymodule
result = mymodule.hybrid_func(10) # OK
# mymodule.c_func(10) # ERROR: not accessible
# From Cython
cdef int result = c_func(10) # Fast C call
result = hybrid_func(10) # Also fast
Type Conversions¶
# Automatic Python ↔ C conversion
cpdef process_data(data):
cdef int n = data # Python → C conversion
cdef double result = compute(n)
return result # C → Python conversion
# Manual conversions
from cpython cimport PyObject
cdef object py_obj = <object>c_pointer
cdef void* c_ptr = <void*>py_obj
Working with Python Objects¶
Efficient String Handling¶
from cpython.bytes cimport PyBytes_AsString, PyBytes_Size
from cpython.unicode cimport PyUnicode_AsUTF8String
def process_string(str text):
"""Convert Python string to C char*"""
cdef bytes py_bytes = text.encode('utf-8')
cdef char* c_string = PyBytes_AsString(py_bytes)
cdef Py_ssize_t length = PyBytes_Size(py_bytes)
# Work with C string
cdef int i
for i in range(length):
# Process c_string[i]
pass
return text.upper()
List and Dict Operations¶
from cpython.list cimport PyList_New, PyList_SET_ITEM, PyList_GET_SIZE
from cpython.dict cimport PyDict_New, PyDict_SetItem, PyDict_GetItem
def fast_list_creation(int n):
"""Create list using C API"""
cdef list result = PyList_New(n)
cdef int i
for i in range(n):
PyList_SET_ITEM(result, i, i * i)
return result
def fast_dict_ops():
"""Dictionary operations using C API"""
cdef dict d = PyDict_New()
cdef object key, value
for i in range(1000):
key = f"key_{i}"
value = i * i
PyDict_SetItem(d, key, value)
return d
Reference Counting¶
from cpython.ref cimport Py_INCREF, Py_DECREF
cdef class ObjectHolder:
cdef object held_object
def __init__(self, obj):
self.held_object = obj
Py_INCREF(obj) # Increase reference count
def __dealloc__(self):
Py_DECREF(self.held_object) # Decrease on cleanup
Extension Types (cdef classes)¶
Basic Extension Type¶
cdef class Vector:
"""High-performance vector class"""
cdef double x, y, z # C-level attributes (fast, no dict overhead)
cdef readonly int dimensions # Read-only from Python
def __init__(self, double x, double y, double z):
self.x = x
self.y = y
self.z = z
self.dimensions = 3
cpdef double dot(self, Vector other):
"""Dot product"""
return self.x * other.x + self.y * other.y + self.z * other.z
cpdef Vector cross(self, Vector other):
"""Cross product"""
return Vector(
self.y * other.z - self.z * other.y,
self.z * other.x - self.x * other.z,
self.x * other.y - self.y * other.x
)
property magnitude:
def __get__(self):
return (self.x**2 + self.y**2 + self.z**2) ** 0.5
def __repr__(self):
return f"Vector({self.x}, {self.y}, {self.z})"
Inheritance and Composition¶
cdef class Shape:
"""Abstract base shape"""
cdef double _area
cdef double _compute_area(self):
"""C-only method for subclasses"""
return 0.0
cpdef double area(self):
if self._area < 0:
self._area = self._compute_area()
return self._area
cdef class Circle(Shape):
cdef double radius
def __init__(self, double radius):
self.radius = radius
self._area = -1
cdef double _compute_area(self):
return 3.14159 * self.radius * self.radius
cdef class Rectangle(Shape):
cdef double width, height
def __init__(self, double width, double height):
self.width = width
self.height = height
self._area = -1
cdef double _compute_area(self):
return self.width * self.height
Interfacing with C Libraries¶
Using Standard C Library¶
from libc.math cimport sin, cos, sqrt, pow, M_PI
from libc.stdlib cimport malloc, free, rand, srand, RAND_MAX
from libc.string cimport memcpy, memset
from libc.stdio cimport printf
def trigonometry(double angle):
"""Use C math functions"""
cdef double rad = angle * M_PI / 180.0
return {
'sin': sin(rad),
'cos': cos(rad),
'tan': sin(rad) / cos(rad)
}
def random_array(int n):
"""Generate random numbers using C rand()"""
cdef int* arr = <int*>malloc(n * sizeof(int))
cdef int i
if arr is NULL:
raise MemoryError()
try:
for i in range(n):
arr[i] = rand() % 100
# Convert to Python list
return [arr[i] for i in range(n)]
finally:
free(arr)
External C Library Integration¶
Create mylib.h:
mylib.h
typedef struct {
double x, y;
} Point2D;
double distance(Point2D* p1, Point2D* p2);
void initialize_library(void);
Create mylib.c:
mylib.c
#include "mylib.h"
#include <math.h>
double distance(Point2D* p1, Point2D* p2) {
double dx = p1->x - p2->x;
double dy = p1->y - p2->y;
return sqrt(dx*dx + dy*dy);
}
void initialize_library(void) {
// Setup code
}
Cython wrapper wrapper.pyx:
cdef extern from "mylib.h":
ctypedef struct Point2D:
double x
double y
double distance(Point2D* p1, Point2D* p2)
void initialize_library()
# Initialize on import
initialize_library()
def calc_distance(double x1, double y1, double x2, double y2):
"""Python interface to C distance function"""
cdef Point2D p1, p2
p1.x = x1
p1.y = y1
p2.x = x2
p2.y = y2
return distance(&p1, &p2)
Update setup.py:
from setuptools import setup, Extension
from Cython.Build import cythonize
extensions = [
Extension(
"wrapper",
sources=["wrapper.pyx", "mylib.c"],
include_dirs=["."],
)
]
setup(ext_modules=cythonize(extensions))
Working with Pointers and Arrays¶
from libc.stdlib cimport malloc, free
def manipulate_pointers():
"""Advanced pointer operations"""
cdef int* int_ptr
cdef double** matrix
cdef int i, j
# Allocate single pointer
int_ptr = <int*>malloc(10 * sizeof(int))
if int_ptr is NULL:
raise MemoryError()
try:
for i in range(10):
int_ptr[i] = i * i
# Allocate 2D array
matrix = <double**>malloc(5 * sizeof(double*))
if matrix is NULL:
raise MemoryError()
for i in range(5):
matrix[i] = <double*>malloc(5 * sizeof(double))
if matrix[i] is NULL:
raise MemoryError()
for j in range(5):
matrix[i][j] = i * j
# Use data...
result = [[matrix[i][j] for j in range(5)] for i in range(5)]
# Free 2D array
for i in range(5):
free(matrix[i])
free(matrix)
return result
finally:
free(int_ptr)
C++ Integration¶
Basic C++ Class Wrapping¶
C++ header shape.hpp:
// shape.hpp
#include <string>
class Shape {
public:
Shape(double x, double y) : x_(x), y_(y) {}
virtual ~Shape() {}
double getX() const { return x_; }
double getY() const { return y_; }
virtual double area() const = 0;
virtual std::string name() const = 0;
protected:
double x_, y_;
};
class Circle : public Shape {
public:
Circle(double x, double y, double radius)
: Shape(x, y), radius_(radius) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
std::string name() const override {
return "Circle";
}
private:
double radius_;
};
Cython wrapper shapes.pyx:
# distutils: language = c++
from libcpp.string cimport string
cdef extern from "shape.hpp":
cdef cppclass Shape:
Shape(double x, double y)
double getX()
double getY()
double area()
string name()
cdef cppclass Circle(Shape):
Circle(double x, double y, double radius)
cdef class PyShape:
"""Python wrapper for C++ Shape"""
cdef Shape* c_shape
def __cinit__(self):
self.c_shape = NULL
def __dealloc__(self):
if self.c_shape != NULL:
del self.c_shape
@property
def x(self):
return self.c_shape.getX()
@property
def y(self):
return self.c_shape.getY()
def area(self):
return self.c_shape.area()
def name(self):
return self.c_shape.name().decode('utf-8')
cdef class PyCircle(PyShape):
"""Python wrapper for C++ Circle"""
def __cinit__(self, double x, double y, double radius):
self.c_shape = new Circle(x, y, radius)
STL Containers¶
# distutils: language = c++
from libcpp.vector cimport vector
from libcpp.map cimport map as cpp_map
from libcpp.set cimport set as cpp_set
from libcpp.string cimport string
from libcpp.pair cimport pair
def use_stl_containers():
"""Demonstrate C++ STL usage"""
cdef vector[int] vec
cdef cpp_map[string, int] word_count
cdef cpp_set[int] unique_nums
# Vector operations
vec.push_back(1)
vec.push_back(2)
vec.push_back(3)
# Map operations
word_count[b"hello"] = 5
word_count[b"world"] = 3
# Set operations
unique_nums.insert(1)
unique_nums.insert(2)
unique_nums.insert(2) # Duplicate ignored
# Convert to Python
py_vec = [vec[i] for i in range(vec.size())]
py_map = {k.decode('utf-8'): v for k, v in word_count}
py_set = {x for x in unique_nums}
return py_vec, py_map, py_set
GIL Management¶
Releasing the GIL¶
from cython.parallel import prange, parallel
import numpy as np
def compute_intensive(double[:] data):
"""Release GIL for CPU-bound operations"""
cdef Py_ssize_t i, n = data.shape[0]
cdef double result = 0.0
# Release GIL - no Python API calls allowed
with nogil:
for i in range(n):
result += data[i] * data[i]
return result
cdef double _nogil_function(double x) nogil:
"""Pure C function that never needs GIL"""
return x * x + 2 * x + 1
def parallel_compute(double[:] data):
"""Parallel computation without GIL"""
cdef Py_ssize_t i, n = data.shape[0]
cdef double[:] result = np.empty(n)
with nogil, parallel():
for i in prange(n):
result[i] = _nogil_function(data[i])
return np.asarray(result)
GIL Acquisition¶
cdef void callback_with_python(void* user_data) nogil:
"""Call Python from C callback"""
with gil:
# Now we can use Python API
py_obj = <object>user_data
py_obj.some_method()
cdef extern from "external.h":
void register_callback(void (*callback)(void*) nogil)
def setup_callback(obj):
"""Register callback that needs Python"""
cdef void* ptr = <void*>obj
register_callback(callback_with_python)
Thread Safety¶
from cpython.ref cimport PyObject
from threading import Lock
cdef class ThreadSafeCounter:
cdef int count
cdef object lock # Python lock object
def __init__(self):
self.count = 0
self.lock = Lock()
def increment(self):
with self.lock:
self.count += 1
def get(self):
with self.lock:
return self.count
cpdef int fast_increment(self):
"""C-level increment (not thread-safe!)"""
self.count += 1
return self.count
Callbacks and Function Pointers¶
# Callback from C to Python
ctypedef void (*c_callback)(double result, void* user_data)
cdef void call_python_callback(double result, void* user_data) with gil:
"""C callback that calls Python"""
callback = <object>user_data
callback(result)
def register_callback(callback_func):
"""Register Python callback"""
cdef void* user_data = <void*>callback_func
# Register with C library
# c_library_register(call_python_callback, user_data)
Memory Management Best Practices¶
from libc.stdlib cimport malloc, calloc, realloc, free
from cpython.mem cimport PyMem_Malloc, PyMem_Free
cdef class ManagedArray:
"""RAII-style memory management"""
cdef double* data
cdef Py_ssize_t size
def __cinit__(self, Py_ssize_t size):
self.size = size
self.data = <double*>PyMem_Malloc(size * sizeof(double))
if self.data is NULL:
raise MemoryError()
def __dealloc__(self):
if self.data is not NULL:
PyMem_Free(self.data)
def __getitem__(self, Py_ssize_t i):
if i < 0 or i >= self.size:
raise IndexError()
return self.data[i]
def __setitem__(self, Py_ssize_t i, double value):
if i < 0 or i >= self.size:
raise IndexError()
self.data[i] = value