Saltar al contenido

Cómo llamar código C/C++ desde CPython (y Pypy) usando Cython y CFFI

Hace unas semanas surgió esta pregunta en StackOverflow en español: ¿Cómo llamar a código C++ desde Python?
Y la respuesta aceptada explica como hacer un wrapper sencillo usando Cython y CFFI. Como da la casualidad que la respuesta es mía voy a extenderla un poco para añadir más cosas y poder explicarla un poco mejor.

Prolegómenos

Antes de empezar a leer esta entrada deberías pasar a leer la entrada que hizo Juanlu hace un tiempo sobre CFFI titulada ‘como crear extensiones en C para Python usando CFFI y Numba‘ donde se dan más detalles de todo el proceso a realizar con CFFI.
Antes de probar el código de la presente entrada deberías instalar cffi y cython:

conda install cffi cython # Válido en CPython

o

pip install cffi cython # Válido en CPython y Pypy

Todo lo que viene a continuación lo he probado en Linux solo usando CPython 3.5 y Pypy 5.1.1, compatible con CPython 2.7 e instalado usando esto.

Preliminares

Antes de pasar a la parte Cython y CFFI vamos a empezar creando los programas C/C++ que vamos a llamar desde Python.
Vamos a crear una librería que lo único que haga será sumar dos números enteros. Haremos una en C/C++ para Cython y una en C/C++ para CFFI.

C/C++ para Cython

C y C++ no son el mismo lenguaje pero para este caso el código se puede considerar el mismo. Para el caso C++ tendremos un fichero *.hpp y un fichero *.cpp (en C sería igual cambiando las extensiones a *.h y *.c, respectivamente).
El fichero *.hpp se llamará milibrería.hpp y contendrá el siguiente código:

long suma_enteros(long n, long m);

Mientras que el fichero *.cpp se llamará milibrería.cpp y contendrá el siguiente código:

long suma_enteros(long n, long m){
    return n + m;
}

Lo que hace el código es bastante simple.

C/C++ para CFFI

En este caso solo vamos a usar un fichero *.cpp y se llamará milibrería_cffi.cpp y contendrá el siguiente código:

long suma_enteros(long n, long m){
    return n + m;
}
extern "C"
{
    extern long cffi_suma_enteros(long n, long m)
    {
        return suma_enteros(n, m);
    }
}

El código es el mismo de antes más una segunda parte que nos permite hacer el código accesible desde Python.

Pegamento entre C/C++ y Python

En esta parte vamos a ver cómo unir el lenguaje compilado con el lenguaje interpretado.

Mediante Cython

Antes de nada necesitamos definir un fichero milibreria.pxd. Este fichero es parecido a lo que hacen los ficheros header en C/C++ o Fortran. Nos ayudará a ‘encontrar’ lo que hemos definido en c++ (más info sobre los ficheros pxd aquí):

cdef extern from "milibreria.hpp":
    long suma_enteros(long n, long m)

Un fichero *.pxd se puede importar en un fichero *.pyx usando la palabra clave cimport
Una vez ‘enlazado’ C/C++ con Cython mediante el fichero *.pxd necesitamos hacer que la parte C/C++ sea accesible desde Python. Para ello creamos el fichero pylibfromcpp.pyx, que es una especie de código Python un poco ‘cythonizado’ (cython es un superconjunto de Python):

cimport milibreria
def suma_enteros(n, m):
    return milibreria.suma_enteros(n, m)

Mediante CFFI

En este caso resulta un poco más sencillo, para este caso concreto. Hemos de crear el fichero Python que, mediante CFFI, enlazará C/C++ con Python. Este ficheros se llamará pylibfromCFFI.py y contendrá el siguiente código.:

import cffi
ffi = cffi.FFI()
ffi.cdef("long cffi_suma_enteros(long n, long m);")
C = ffi.dlopen("./milibreria.so")
def suma_enteros(n, m):
    return C.cffi_suma_enteros(n, m)

Setup

Compilando con Cython

Para poder acceder a la librería C/C++ hemos de crear un fichero setup.py que se encargará de la compilación que permitirá crear la extensión a la que accederemos desde Python. El fichero setup.py contendrá:

from distutils.core import setup, Extension
from Cython.Build import cythonize
ext = Extension("pylibfromcpp",
              sources=["pylibfromcpp.pyx", "milibreria.cpp"],
              language="c++",)
setup(name = "cython_pylibfromcpp",
      ext_modules = cythonize(ext))

Para crear la extensión en sí, en la misma carpeta donde hemos dejado todos los ficheros anteriores y desde la línea de comandos, hacemos (como siempre, recomiendo hacer esto desde un entorno virtual):

python setup.py build_ext -i

Y debería aparecer un fichero pylibfromcpp.cpp y otro fichero pylibfromcpp.pypy-41.so en la misma carpeta donde habéis ejecutado el comando anterior.

Compilando con CFFI

Para poder hacer accesible la funcionalidad definida en C/C++ desde Python podemos compilar usando:

g++ -o ./milibreria.so ./milibreria_cffi.cpp -fPIC -shared

Y deberíamos obtener el fichero milibreria.so.

Llamando desde Python

Usando nuestro ‘wrapper’ Cython

Ahora, si todo ha salido bien, dentro de un intérprete de python (como he comentado más arriba, lo he probado con CPython 3.5 y Pypy 5.1.1 y me ha funcionado en ambos) podemos hacer:

import pylibfromcpp
print(pylibfromcpp.suma_enteros(2, 3))

Usando nuestro ‘wrapper’ CFFI

De igual forma, si todo ha salido bien, podemos hacer:

import pylibfromcpp
print(pylibfromcpp.suma_enteros(2, 3))

Output completo en la consola pypy

Para el caso Cython

Python 2.7.10 (b0a649e90b6642251fb4a765fe5b27a97b1319a9, May 05 2016, 17:21:19)
[PyPy 5.1.1 with GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>> import pylibfromcpp
>>>> print(pylibfromcpp.suma_enteros(2, 3))
5

Para el caso CFFI

Python 2.7.10 (b0a649e90b6642251fb4a765fe5b27a97b1319a9, May 05 2016, 17:21:19)
[PyPy 5.1.1 with GCC 4.9.2] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>>> import pylibfromCFFI
>>>> print(pylibfromCFFI.suma_enteros(2, 3))
5

Comentarios finales

Un esquema, grosso modo, de lo que hemos hecho:

Pros y contras de cada una de las aproximaciones:

  • Cython permite usar Numpy sin problemas en CPython. Sin embargo, la última vez que intenté usar código Python con numpy arrays (Cython + Numpypy) reventaba todo en Pypy.
  • Cython lo podemos usar con CPython 2.x y 3.x. Cython funciona sin problemas en Pypy 5.1.1 (compatible con CPython 2.7). Numpypy NO funciona en Pypy3k.
  • El wrapper Cython que hemos hecho en este ejercicio es claramente más complejo que el que hemos hecho con CFFI (en este caso concreto).
  • Con Cython podemos usar el código compilado sin tocarlo mientras que con CFFI hemos de crear algo de código (muy simple) en el lenguaje compilado para acceder a su funcionalidad.
  • CFFI permite usar numpy arrays de forma sencilla, aunque, como con Cython, hay que ‘ayudar con algo de código no Python’ para que todo se pueda comunicar correctamente.

Documentación

Cython.
CFFI.

3 comentarios en «Cómo llamar código C/C++ desde CPython (y Pypy) usando Cython y CFFI»

  1. Pingback: Cómo llamar código C/C++ desde CP...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

− five = two

Pybonacci