Envolviendo una biblioteca de C en Python: C, Cython o ctypes?

Quiero llamar a una biblioteca de C desde una aplicación de Python. No quiero envolver toda la API, solo las funciones y tipos de datos que son relevantes para mi caso. Como lo veo, tengo tres opciones:

  1. Cree un módulo de extensión real en C. Probablemente sea excesivo, y también me gustaría evitar la sobrecarga de aprender a escribir extensión.
  2. Utilice Cython para exponer las partes relevantes de la biblioteca de C a Python.
  3. Realice todo el proceso en Python, utilizando ctypes para comunicarse con la biblioteca externa.

No estoy seguro de si 2) o 3) es la mejor opción. La ventaja de 3) es que ctypes es parte de la biblioteca estándar, y el código resultante sería Python puro, aunque no estoy seguro de cuánta es esa ventaja.

¿Hay más ventajas / desventajas con cualquiera de las dos opciones? ¿Qué enfoque me recomienda?


Edit: Gracias por todas sus respuestas, proporcionan un buen recurso para cualquiera que busque hacer algo similar. La decisión, por supuesto, aún debe tomarse para el caso individual, no hay una respuesta de tipo “Esto es lo correcto”. Para mi propio caso, probablemente iré con ctypes, pero también estoy ansioso por probar Cython en algún otro proyecto.

Dado que no hay una sola respuesta verdadera, aceptar una es algo arbitrario; Elegí la respuesta de FogleBird ya que proporciona una buena perspectiva de los tipos de virus y actualmente también es la respuesta más votada. Sin embargo, sugiero leer todas las respuestas para obtener una buena visión general.

Gracias de nuevo.

ctypes es su mejor apuesta para hacerlo rápidamente, ¡y es un placer trabajar con él mientras aún está escribiendo Python!

Recientemente envolví un controlador FTDI para comunicarme con un chip USB usando ctypes y fue genial. Lo hice todo y trabajé en menos de un día laboral. (Solo implementé las funciones que necesitábamos, unas 15 funciones).

Anteriormente utilizábamos un módulo de terceros, PyUSB , para el mismo propósito. PyUSB es un módulo de extensión C / Python real. Pero PyUSB no estaba lanzando el GIL cuando hacía lockings de lecturas / escrituras, lo que nos estaba causando problemas. Así que escribí nuestro propio módulo usando ctypes, que libera la GIL al llamar a las funciones nativas.

Una cosa a tener en cuenta es que los ctypes no sabrán sobre las constantes de #define y las cosas de la biblioteca que está usando, solo las funciones, por lo que tendrá que redefinir esas constantes en su propio código.

Aquí hay un ejemplo de cómo terminó el código (muchos se cortaron, solo tratando de mostrarte lo esencial):

 from ctypes import * d2xx = WinDLL('ftd2xx') OK = 0 INVALID_HANDLE = 1 DEVICE_NOT_FOUND = 2 DEVICE_NOT_OPENED = 3 ... def openEx(serial): serial = create_string_buffer(serial) handle = c_int() if d2xx.FT_OpenEx(serial, OPEN_BY_SERIAL_NUMBER, byref(handle)) == OK: return Handle(handle.value) raise D2XXException class Handle(object): def __init__(self, handle): self.handle = handle ... def read(self, bytes): buffer = create_string_buffer(bytes) count = c_int() if d2xx.FT_Read(self.handle, buffer, bytes, byref(count)) == OK: return buffer.raw[:count.value] raise D2XXException def write(self, data): buffer = create_string_buffer(data) count = c_int() bytes = len(data) if d2xx.FT_Write(self.handle, buffer, bytes, byref(count)) == OK: return count.value raise D2XXException 

Alguien hizo algunos puntos de referencia en las diversas opciones.

Podría dudar más si tuviera que envolver una biblioteca de C ++ con muchas clases / plantillas / etc. Pero los ctypes funcionan bien con estructuras e incluso pueden volver a llamar a Python.

Advertencia: la opinión de un desarrollador de Cython Core por delante.

Casi siempre recomiendo Cython sobre ctypes. La razón es que tiene una ruta de actualización mucho más suave. Si usa ctypes, muchas cosas serán simples al principio, y ciertamente es bueno escribir su código FFI en Python simple, sin comstackción, dependencias de comstackción y todo eso. Sin embargo, en algún momento, es casi seguro que tendrá que llamar mucho a su biblioteca de C, ya sea en un bucle o en una serie más larga de llamadas interdependientes, y le gustaría acelerar eso. Ese es el punto en el que notará que no puede hacer eso con ctypes. O, cuando necesita funciones de callback y encuentra que su código de callback de Python se convierte en un cuello de botella, le gustaría acelerarlo y / o moverlo a C también. Nuevamente, no puedes hacer eso con ctypes. Por lo tanto, tiene que cambiar los idiomas en ese punto y comenzar a reescribir partes de su código, lo que podría hacer ingeniería inversa de su código Python / ctypes en C simple, lo que arruina todo el beneficio de escribir su código en Python simple en primer lugar.

Con Cython, OTOH, eres completamente libre de hacer que el código de ajuste y de llamada sea tan fino o grueso como quieras. Puede comenzar con llamadas simples a su código C desde el código regular de Python, y Cython las convertirá en llamadas nativas de C, sin gastos generales de llamadas adicionales, y con una sobrecarga de conversión extremadamente baja para los parámetros de Python. Cuando note que necesita aún más rendimiento en algún momento en el que está realizando demasiadas llamadas costosas a su biblioteca de C, puede comenzar a anotar su código Python circundante con tipos estáticos y dejar que Cython lo optimice directamente hacia C para usted. O bien, puede comenzar a reescribir partes de su código C en Cython para evitar llamadas y especializarse y ajustar sus bucles de forma algorítmica. Y si necesita una callback rápida, simplemente escriba una función con la firma apropiada y pásela directamente en el registro de callback en C. Una vez más, no hay gastos generales, y le da un rendimiento de llamadas C simple. Y en el caso mucho menos probable de que realmente no pueda obtener su código lo suficientemente rápido en Cython, aún puede considerar reescribir las partes verdaderamente críticas de él en C (o C ++ o Fortran) y llamar desde su código de Cython de forma natural y nativa. Pero entonces, esto realmente se convierte en el último recurso en lugar de la única opción.

Entonces, ctypes es bueno hacer cosas simples y hacer que algo se ejecute rápidamente. Sin embargo, tan pronto como las cosas empiecen a crecer, lo más probable es que llegues al punto en el que te des cuenta de que es mejor que uses Cython desde el principio.

Cython es una herramienta muy buena en sí misma, bien vale la pena aprender, y está sorprendentemente cerca de la syntax de Python. Si realiza cualquier cálculo científico con Numpy, Cython es el camino a seguir, ya que se integra con Numpy para operaciones matriciales rápidas.

Cython es un superconjunto del lenguaje Python. Puede lanzar cualquier archivo Python válido y escupirá un progtwig C válido. En este caso, Cython solo asignará las llamadas de Python a la API de CPython subyacente. Esto se traduce quizás en una aceleración del 50% porque su código ya no se interpreta.

Para obtener algunas optimizaciones, debe comenzar a contarle a Cython datos adicionales sobre su código, como declaraciones de tipo. Si le dices lo suficiente, puede reducir el código a C pura. Es decir, un bucle for en Python se convierte en un bucle for en C. Aquí verás ganancias masivas de velocidad. También puede enlazar a progtwigs externos de C aquí.

Usar el código Cython también es increíblemente fácil. Pensé que el manual hace que suene difícil. Literalmente solo haces:

 $ cython mymodule.pyx $ gcc [some arguments here] mymodule.c -o mymodule.so 

y luego puedes import mymodule en tu código de Python y olvidarte por completo que se comstack hasta C.

En cualquier caso, dado que Cython es tan fácil de configurar y comenzar a usar, sugiero que lo intente para ver si se ajusta a sus necesidades. No será un desperdicio si resulta que no es la herramienta que está buscando.

Para llamar a una biblioteca de C desde una aplicación Python también hay cffi, que es una nueva alternativa para ctypes . Trae un aspecto fresco para FFI:

  • maneja el problema de una manera fascinante y limpia (a diferencia de los ctypes )
  • no requiere escribir código no Python (como en SWIG, Cython , …)

Voy a tirar otro por ahí: SWIG

Es fácil de aprender, hace muchas cosas bien y es compatible con muchos más idiomas, por lo que el tiempo empleado en aprenderlo puede ser bastante útil.

Si usas SWIG, estás creando un nuevo módulo de extensión de python, pero con SWIG haciendo la mayor parte del trabajo pesado por ti.

Personalmente, escribiría un módulo de extensión en C. No se deje intimidar por las extensiones de Python C: no son difíciles de escribir. La documentación es muy clara y útil. Cuando escribí por primera vez una extensión C en Python, creo que me llevó aproximadamente una hora descubrir cómo escribir una, no mucho tiempo.

ctypes es genial cuando ya tienes un blob de biblioteca comstackdo para tratar (como las bibliotecas de SO). Sin embargo, la sobrecarga de llamadas es grave, por lo que si va a hacer muchas llamadas a la biblioteca y va a escribir el código C de todos modos (o al menos comstackrlo), diría que vaya a por cython . No es mucho más trabajo, y será mucho más rápido y más python utilizar el archivo pyd resultante.

Personalmente tiendo a usar cython para acelerar rápidamente el código de Python (los bucles y las comparaciones de enteros son dos áreas en las que cython brilla particularmente), y cuando exista algo más de código / ajuste de otras bibliotecas involucradas, recurriré a Boost.Python . Boost.Python puede ser complicado de configurar, pero una vez que lo tienes funcionando, simplifica el ajuste del código C / C ++.

cython también es genial para envolver numpy (que aprendí en los procedimientos de SciPy 2009 ), pero no he usado numpy, así que no puedo comentar sobre eso.

Si ya tiene una biblioteca con una API definida, creo que ctypes es la mejor opción, ya que solo tiene que hacer un poco de inicialización y luego más o menos llamar a la biblioteca de la forma en que está acostumbrado.

Creo que Cython o la creación de un módulo de extensión en C (que no es muy difícil) son más útiles cuando necesitas un nuevo código, por ejemplo, llamar a esa biblioteca y hacer algunas tareas complejas que requieren mucho tiempo, y luego pasar el resultado a Python.

Otro enfoque, para progtwigs simples, es hacer directamente un proceso diferente (comstackdo externamente), emitir el resultado a la salida estándar y llamarlo con el módulo de subproceso. A veces es el enfoque más fácil.

Por ejemplo, si creas un progtwig de consola C que funcione más o menos de esa manera

 $miCcode 10 Result: 12345678 

Podrías llamarlo desde Python

 >>> import subprocess >>> p = subprocess.Popen(['miCcode', '10'], shell=True, stdout=subprocess.PIPE) >>> std_out, std_err = p.communicate() >>> print std_out Result: 12345678 

Con un poco de formación de cadena, puede tomar el resultado de la forma que desee. También puede capturar la salida de error estándar, por lo que es bastante flexible.

Hay un problema que me hizo usar ctypes y no cython y que no se menciona en otras respuestas.

Usando ctypes, el resultado no depende en absoluto del comstackdor que esté usando. Puede escribir una biblioteca utilizando más o menos cualquier idioma que pueda comstackrse en una biblioteca compartida nativa. No importa mucho, qué sistema, qué idioma y qué comstackdor. Cython, sin embargo, está limitado por la infraestructura. Por ejemplo, si desea utilizar el comstackdor de Intel en Windows, es mucho más complicado hacer que funcione cython: debe “explicar” el comstackdor a cython, comstackr algo con este comstackdor exacto, etc. Lo que limita significativamente la portabilidad.

Si está apuntando a Windows y elige envolver algunas bibliotecas propietarias de C ++, pronto descubrirá que las diferentes versiones de msvcrt***.dll (Visual C ++ Runtime) son ligeramente incompatibles.

Esto significa que es posible que no pueda usar Cython ya que el wrapper.pyd resultante está vinculado contra msvcr90.dll (Python 2.7) o msvcr100.dll (Python 3.x) . Si la biblioteca que está envolviendo está vinculada a una versión diferente del tiempo de ejecución, no tendrá suerte.

Luego, para hacer que las cosas funcionen, deberá crear envolturas de C para las bibliotecas de C ++, vincular esa dll de envoltura con la misma versión de msvcrt***.dll que su biblioteca de C ++. Y luego use ctypes para cargar su dll de envoltura enrollada a mano dinámicamente en el tiempo de ejecución.

Así que hay muchos detalles pequeños, que se describen con gran detalle en el siguiente artículo:

“Bibliotecas nativas hermosas (en Python) “: http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/

También hay una posibilidad de usar GObject Introspection para las bibliotecas que usan GLib .