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.
Pingback: Cómo llamar código C/C++ desde CP...
¡Brutal artículo Kiko! Resultará muy útil a muchos seguro 😀
Se viene uno con Fortran en el que serás coautor 😛