Avvolgere una libreria C in Python: C, Cython o ctypes?


284

Voglio chiamare una libreria C da un'applicazione Python. Non voglio racchiudere l'intera API, solo le funzioni e i tipi di dati rilevanti per il mio caso. A mio avviso, ho tre opzioni:

  1. Crea un modulo di estensione reale in C. Probabilmente eccessivo, e vorrei anche evitare il sovraccarico di apprendimento della scrittura di estensione.
  2. Utilizzare Cython per esporre le parti rilevanti dalla libreria C a Python.
  3. Fai tutto in Python, usando ctypesper comunicare con la libreria esterna.

Non sono sicuro se 2) o 3) sia la scelta migliore. Il vantaggio di 3) è che ctypesfa parte della libreria standard e il codice risultante sarebbe Python puro, anche se non sono sicuro di quanto sia effettivamente grande quel vantaggio.

Ci sono più vantaggi / svantaggi con entrambe le scelte? Quale approccio mi consigliate?


Modifica: grazie per tutte le tue risposte, forniscono una buona risorsa per chiunque cerchi di fare qualcosa di simile. La decisione, ovviamente, deve ancora essere presa per il singolo caso: non esiste una sorta di risposta "Questa è la cosa giusta". Per il mio caso, probabilmente andrò con i tipi, ma non vedo l'ora di provare Cython in qualche altro progetto.

Non essendoci una sola risposta vera, accettarne una è in qualche modo arbitrario; Ho scelto la risposta di FogleBird in quanto fornisce una buona visione dei tipi e attualmente è anche la risposta più votata. Tuttavia, suggerisco di leggere tutte le risposte per ottenere una buona panoramica.

Grazie ancora.


3
In una certa misura, l'applicazione specifica coinvolta (ciò che la libreria fa) può influenzare la scelta dell'approccio. Abbiamo usato con successo i ctypes per parlare con DLL fornite dal fornitore per vari pezzi di hardare (ad esempio oscilloscopi) ma non avrei necessariamente scelto prima i ctypes per parlare con una libreria di elaborazione numerica, a causa dell'overhead aggiuntivo rispetto a Cython o SWIG.
Peter Hansen,

1
Ora hai quello che stavi cercando. Quattro risposte diverse (qualcuno ha anche trovato SWIG). Ciò significa che ora hai 4 opzioni invece di 3.
Luka Rahne,

@ralu Questo è quello che ho pensato anche io :-) Ma sul serio, non mi aspettavo (o volevo) un tavolo pro / contro o una sola risposta che dicesse "Ecco cosa devi fare". Ad ogni domanda sul processo decisionale si risponde al meglio con i "fan" di ogni possibile scelta, fornendo le loro ragioni. Il voto della comunità fa quindi la sua parte, così come il mio lavoro (guardare gli argomenti, applicarli al mio caso, leggere le fonti fornite, ecc.). Per farla breve: ci sono alcune buone risposte qui.
Balpha,

Quindi con quale approccio hai intenzione di seguire? :)
FogleBird il

1
Per quanto ne so (per favore correggimi se sbaglio), Cython è un fork di Pyrex con più sviluppo in corso, che rende Pyrex praticamente obsoleto.
Balpha,

Risposte:


115

ctypes è la soluzione migliore per farlo rapidamente, ed è un piacere lavorare mentre stai ancora scrivendo Python!

Di recente ho avvolto un driver FTDI per comunicare con un chip USB usando i ctypes ed è stato fantastico. Ho fatto tutto e lavorato in meno di un giorno lavorativo. (Ho implementato solo le funzioni di cui avevamo bisogno, circa 15 funzioni).

In precedenza utilizzavamo un modulo di terze parti, PyUSB , per lo stesso scopo. PyUSB è un vero modulo di estensione C / Python. Ma PyUSB non stava rilasciando GIL quando faceva blocchi di lettura / scrittura, il che stava causando problemi per noi. Quindi ho scritto il nostro modulo usando ctypes, che rilascia il GIL quando chiama le funzioni native.

Una cosa da notare è che i tipi non lo sapranno #define costanti e le cose nella libreria che stai usando, solo le funzioni, quindi dovrai ridefinire quelle costanti nel tuo codice.

Ecco un esempio di come il codice ha finito per apparire (molti frammenti, solo cercando di mostrarti l'essenza di esso):

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

Qualcuno ha fatto alcuni benchmark sulle varie opzioni.

Potrei essere più titubante se dovessi avvolgere una libreria C ++ con molte classi / modelli / ecc. Ma i ctypes funzionano bene con le strutture e possono persino richiamare in Python.


5
Unendo gli elogi per i tipi, ma noti un problema (non documentato): i tipi non supportano il fork. Se si passa da un processo usando i ctypes e entrambi i processi padre e figlio continuano a utilizzare i ctypes, ci si imbatte in un brutto bug che ha a che fare con i ctypes usando la memoria condivisa.
Oren Shemesh,

1
@OrenShemesh Ci sono ulteriori letture su questo argomento che puoi indicarmi? Penso che potrei essere al sicuro con un progetto a cui sto attualmente lavorando, poiché credo che solo il processo genitore usi ctypes(per pyinotify), ma mi piacerebbe capire il problema in modo più approfondito.
zigg

Questo passaggio mi aiuta molto One thing to note is that ctypes won't know about #define constants and stuff in the library you're using, only the functions, so you'll have to redefine those constants in your own code.Quindi, devo definire le costanti presenti in winioctl.h....
swdev,

che ne dici di prestazioni? ctypesè molto più lento dell'estensione C poiché il collo di bottiglia è l'interfaccia da Python a C
TomSawyer,

154

Avvertenza: un'opinione dello sviluppatore del core Cython a venire.

Raccomando quasi sempre Cython rispetto ai tipi. Il motivo è che ha un percorso di aggiornamento molto più fluido. Se usi i ctypes, molte cose all'inizio saranno semplici, ed è certamente bello scrivere il tuo codice FFI in Python, senza compilazione, costruire dipendenze e tutto il resto. Tuttavia, ad un certo punto, quasi sicuramente scoprirai che devi chiamare molto nella tua libreria C, in un ciclo o in una serie più lunga di chiamate interdipendenti, e vorresti accelerarlo. Questo è il punto in cui noterai che non puoi farlo con i tipi. Oppure, quando hai bisogno di funzioni di callback e scopri che il tuo codice di callback Python diventa un collo di bottiglia, ti piacerebbe velocizzarlo e / o spostarlo anche in C. Ancora una volta, non puoi farlo con i tipi.

Con Cython, OTOH, sei completamente libero di rendere il codice di wrapping e di chiamata sottile o spesso come desideri. Puoi iniziare con semplici chiamate nel tuo codice C dal normale codice Python e Cython le tradurrà in chiamate C native, senza alcun sovraccarico di chiamata aggiuntivo e con un sovraccarico di conversione estremamente basso per i parametri Python. Quando noti che hai bisogno di prestazioni ancora maggiori ad un certo punto in cui stai effettuando troppe chiamate costose nella tua libreria C, puoi iniziare ad annotare il tuo codice Python circostante con tipi statici e lasciare che Cython lo ottimizzi direttamente in C per te. In alternativa, è possibile iniziare a riscrivere parti del codice C in Cython per evitare chiamate e specializzare e rafforzare i loop in modo algoritmico. E se hai bisogno di una richiamata veloce, basta scrivere una funzione con la firma appropriata e passarla direttamente nel registro di callback C. Ancora una volta, nessun sovraccarico, e ti dà prestazioni di chiamata in chiaro C. E nel caso molto meno probabile che non riesci davvero a ottenere il tuo codice abbastanza velocemente in Cython, puoi comunque considerare di riscriverne le parti veramente critiche in C (o C ++ o Fortran) e chiamarlo dal tuo codice Cython in modo naturale e nativo. Ma poi, questo diventa davvero l'ultima risorsa anziché l'unica opzione.

Quindi, ctypes è bello fare cose semplici e far funzionare rapidamente qualcosa. Tuttavia, non appena le cose iniziano a crescere, molto probabilmente arriverai al punto in cui noti che faresti meglio a usare Cython fin dall'inizio.


4
+1 quelli sono buoni punti, grazie mille! Anche se mi chiedo se spostare solo le parti del collo di bottiglia su Cython sia davvero un sovraccarico. Ma sono d'accordo, se ti aspetti qualche tipo di problema di prestazioni, potresti utilizzare Cython sin dall'inizio.
balpha,

Questo vale ancora per i programmatori esperti con C e Python? In tal caso si potrebbe sostenere che Python / ctypes è la scelta migliore, poiché la vettorializzazione dei loop C (SIMD) è talvolta più semplice. Ma, a parte questo, non riesco a pensare ad alcun inconveniente di Cython.
Alex van Houten,

Grazie per la risposta! Una cosa di cui ho avuto problemi riguardo a Cython è ottenere il giusto processo di compilazione (ma ciò ha a che fare anche con me che non scrivo mai un modulo Python prima) - dovrei compilarlo prima o includere i file sorgente di Cython in sdist e domande simili. Ho scritto un post sul blog nel caso in cui qualcuno abbia problemi / dubbi simili: martinsosic.com/development/2016/02/08/…
Martinsos,

Grazie per la risposta! Uno svantaggio quando uso Cython è che il sovraccarico dell'operatore non è completamente implementato (ad es __radd__.). Ciò è particolarmente fastidioso quando pianifichi che la tua classe interagisca con i tipi predefiniti (ad es. intE float). Inoltre, i metodi magici in cython sono solo un po 'buggy in generale.
Monolito,

100

Cython è uno strumento piuttosto interessante in sé, vale la pena imparare ed è sorprendentemente vicino alla sintassi di Python. Se esegui calcoli scientifici con Numpy, Cython è la strada da percorrere perché si integra con Numpy per operazioni a matrice veloce.

Cython è un superset del linguaggio Python. Puoi lanciare qualsiasi file Python valido e sputerà un programma C valido. In questo caso, Cython eseguirà il mapping delle chiamate Python all'API CPython sottostante. Ciò comporta forse un aumento del 50% perché il codice non viene più interpretato.

Per ottenere alcune ottimizzazioni, devi iniziare a dire a Cython ulteriori informazioni sul tuo codice, come le dichiarazioni di tipo. Se lo dici abbastanza, può ridurre il codice in puro C. Cioè, un ciclo for in Python diventa un ciclo for in C. Qui vedrai enormi guadagni di velocità. Puoi anche collegarti a programmi C esterni qui.

L'uso del codice Cython è anche incredibilmente facile. Ho pensato che il manuale potesse sembrare difficile. Letteralmente fai:

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

e poi puoi import mymodulenel tuo codice Python e dimenticare completamente che si compila fino a C.

In ogni caso, poiché Cython è così facile da configurare e iniziare a utilizzare, suggerisco di provarlo per vedere se soddisfa le tue esigenze. Non sarà uno spreco se si scopre che non è lo strumento che stai cercando.


1
Nessun problema. La cosa bella di Cython è che puoi imparare solo ciò di cui hai bisogno. Se vuoi solo un modesto miglioramento, tutto ciò che devi fare è compilare i tuoi file Python e il gioco è fatto.
Carl

18
"Puoi lanciare qualsiasi file Python valido e sputerà un programma C valido." <- Non del tutto, ci sono alcune limitazioni: docs.cython.org/src/userguide/limitations.html Probabilmente non è un problema per la maggior parte dei casi d'uso, ma voleva solo essere completo.
Randy Syring,

7
I problemi stanno diminuendo con ogni versione, al punto che quella pagina ora dice "la maggior parte dei problemi è stata risolta in 0.15".
Henry Gomersall,

3
Per aggiungere, esiste un modo ANCORA più semplice per importare il codice cython: scrivere il codice cython come mymod.pyxmodulo e poi fare import pyximport; pyximport.install(); import mymode la compilazione avviene dietro le quinte.
Kaushik Ghose,

3
@kaushik Ancora più semplice è pypi.python.org/pypi/runcython . Basta usare runcython mymodule.pyx. A differenza di pyximport, è possibile utilizzarlo per attività di collegamento più impegnative. L'unica avvertenza è che sono io quello che ha scritto le 20 righe di bash per questo e potrebbe essere di parte.
RussellStewart,

42

Per chiamare una libreria C da un'applicazione Python c'è anche cffi che è una nuova alternativa per i tipi . Porta un nuovo look per FFI:

  • gestisce il problema in modo affascinante e pulito (al contrario dei tipi )
  • non richiede la scrittura di codice non Python (come in SWIG, Cython , ...)

sicuramente la strada da percorrere per il confezionamento , come voleva OP. cython suona alla grande per scrivere loro stessi hot loop, ma per le interfacce, cffi è semplicemente un aggiornamento diretto dai tipi.
pecore volanti,

21

Ne lancerò un altro là fuori: SWIG

È facile da imparare, fa molte cose nel modo giusto e supporta molte più lingue, quindi il tempo dedicato all'apprendimento può essere molto utile.

Se usi SWIG, stai creando un nuovo modulo di estensione Python, ma con SWIG che fa la maggior parte del lavoro pesante per te.


18

Personalmente, scriverei un modulo di estensione in C. Non fatevi intimidire dalle estensioni di Python C: non sono affatto difficili da scrivere. La documentazione è molto chiara e utile. Quando ho scritto per la prima volta un'estensione C in Python, penso che mi ci sia voluta circa un'ora per capire come scriverne una, non molto tempo.


Avvolgimento di una libreria C. Puoi effettivamente trovare il codice qui: github.com/mdippery/lehmer
mipadi

1
@forivall: il codice non era poi così utile e ci sono migliori generatori di numeri casuali là fuori. Ho solo un backup sul mio computer.
mipadi,

2
Concordato. L'API C di Python non è così spaventosa come sembra (supponendo che tu conosca C). Tuttavia, a differenza di Python e del suo serbatoio di librerie, risorse e sviluppatori, quando scrivi estensioni in C sei praticamente solo. Probabilmente è l'unico inconveniente (oltre a quelli che in genere vengono con la scrittura in C).
Noob Saibot,

1
@mipadi: bene, ma differiscono tra Python 2.xe 3.x, quindi è più conveniente utilizzare Cython di scrivere la propria estensione, hanno Cython figura tutti i dettagli e quindi compilare il codice C generato per Python 2.x o 3.x secondo necessità.
0xC0000022L

2
@mipadi sembra che il link github sia morto e non sembra disponibile su archive.org, hai un backup?
jrh

11

ctypes è fantastico quando hai già un BLOB di librerie compilato da gestire (come le librerie del sistema operativo). L'overhead delle chiamate è grave, tuttavia, quindi se effettuerai molte chiamate nella libreria e scriverai comunque il codice C (o almeno lo compili), direi di andare per cython . Non è molto più lavoro e sarà molto più veloce e più pitone utilizzare il file pyd risultante.

Personalmente tendo a usare cython per velocizzare rapidamente il codice python (i cicli e i confronti di numeri interi sono due aree in cui il cython brilla in modo particolare), e quando c'è un po 'di codice / wrapping più coinvolto di altre librerie coinvolte, mi rivolgerò a Boost.Python . Boost.Python può essere complicato da configurare, ma una volta che ha funzionato, rende semplice il wrapping del codice C / C ++.

cython è anche bravissimo a impacchettare numpy (che ho imparato dagli atti di SciPy 2009 ), ma non ho usato numpy, quindi non posso commentarlo.


11

Se hai già una libreria con un'API definita, penso che ctypessia l'opzione migliore, poiché devi solo fare una piccola inizializzazione e quindi più o meno chiamare la libreria come sei abituato.

Penso che Cython o la creazione di un modulo di estensione in C (che non è molto difficile) siano più utili quando è necessario un nuovo codice, ad esempio chiamare quella libreria e svolgere alcune attività complesse e dispendiose in termini di tempo, quindi passare il risultato a Python.

Un altro approccio, per programmi semplici, è eseguire direttamente un processo diverso (compilato esternamente), trasmettendo il risultato all'output standard e chiamandolo con il modulo di sottoprocesso. A volte è l'approccio più semplice.

Ad esempio, se si crea un programma console C che funziona più o meno in quel modo

$miCcode 10
Result: 12345678

Potresti chiamarlo da 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 una piccola formattazione di stringhe, puoi ottenere il risultato nel modo che preferisci. Puoi anche acquisire l'output di errore standard, quindi è abbastanza flessibile.


Sebbene non vi sia nulla di errato in questa risposta, le persone dovrebbero essere caute se il codice deve essere aperto per l'accesso da parte di altri poiché la chiamata al sottoprocesso shell=Truepotrebbe facilmente comportare un qualche tipo di exploit quando un utente ottiene davvero una shell. Va bene quando lo sviluppatore è l'unico utente, ma nel mondo ci sono un sacco di fastidiosi cazzi che aspettano qualcosa di simile.
Ben

7

C'è un problema che mi ha fatto usare i ctypes e non il cython e che non è menzionato in altre risposte.

Utilizzando ctypes il risultato non dipende affatto dal compilatore che si sta utilizzando. È possibile scrivere una libreria utilizzando più o meno qualsiasi lingua che può essere compilata nella libreria condivisa nativa. Non importa molto, quale sistema, quale lingua e quale compilatore. Cython, tuttavia, è limitato dall'infrastruttura. Ad esempio, se si desidera utilizzare il compilatore Intel su Windows, è molto più complicato far funzionare cython: è necessario "spiegare" il compilatore a cython, ricompilare qualcosa con questo compilatore esatto, ecc. Ciò limita in modo significativo la portabilità.


4

Se stai prendendo di mira Windows e scegli di racchiudere alcune librerie C ++ proprietarie, potresti presto scoprire che versioni diverse di msvcrt***.dll(Visual C ++ Runtime) sono leggermente incompatibili.

Ciò significa che potresti non essere in grado di utilizzare Cythonpoiché il risultato wrapper.pydè collegato a msvcr90.dll (Python 2.7) o msvcr100.dll (Python 3.x) . Se la libreria che stai racchiudendo è collegata a una versione diversa del runtime, sei sfortunato.

Quindi per far funzionare le cose dovrai creare wrapper C per le librerie C ++, collegare quella wrapper dll alla stessa versione della msvcrt***.dlltua libreria C ++. Quindi utilizzare ctypesper caricare dinamicamente la dll del wrapper arrotolato a mano in fase di esecuzione.

Quindi ci sono molti piccoli dettagli, che sono descritti in dettaglio nel seguente articolo:

"Belle librerie native (in Python) ": http://lucumr.pocoo.org/2013/8/18/beautiful-native-libraries/


Questo articolo non ha nulla a che fare con i problemi sollevati con la compatibilità dei compilatori Microsoft. Far funzionare le estensioni Cython su Windows non è molto difficile. Sono stato in grado di utilizzare MinGW per praticamente tutto. Una buona distribuzione di Python aiuta però.
IanH

2
+1 per menzionare un possibile problema su Windows (che attualmente sto riscontrando anche ...). @IanH è meno su Windows in generale, ma è un casino se sei bloccato con una data lib di terze parti che non corrisponde alla tua distribuzione Python.
sebastian


2

So che questa è una vecchia domanda, ma questa cosa si presenta su Google quando cerchi cose del genere ctypes vs cython, e la maggior parte delle risposte qui sono scritte da coloro che sono già esperti cythono cche potrebbero non riflettere il tempo effettivo necessario per investire per imparare quelle per implementare la tua soluzione. Sono un principiante assoluto in entrambi. Non ho mai toccato cythonprima e ho poca esperienza c/c++.

Negli ultimi due giorni, stavo cercando un modo per delegare una parte pesante del mio codice a un livello più basso rispetto a Python. Ho implementato il mio codice sia in ctypesche Cython, che consisteva sostanzialmente di due semplici funzioni.

Ho avuto un enorme elenco di stringhe che doveva essere elaborato. Avviso liste string. Entrambi i tipi non corrispondono perfettamente ai tipi in c, perché le stringhe di Python sono di default Unicode e le cstringhe no. Le liste in Python non sono semplicemente matrici di c.

Ecco il mio verdetto. Usa cython. Si integra in modo più fluido con Python e più facile da lavorare in generale. Quando qualcosa va storto ctypesti lancia segfault, almeno cythonti darà avvisi di compilazione con una traccia dello stack ogni volta che è possibile, e puoi restituire facilmente un oggetto Python valido cython.

Ecco un resoconto dettagliato su quanto tempo ho avuto bisogno di investire in entrambi per implementare la stessa funzione. A proposito, ho fatto pochissima programmazione C / C ++:

  • ctypes:

    • Circa 2 ore sulla ricerca di come trasformare il mio elenco di stringhe unicode in un tipo compatibile ac.
    • Circa un'ora su come restituire correttamente una stringa dalla funzione ac. Qui ho effettivamente fornito la mia soluzione a SO una volta che ho scritto le funzioni.
    • Circa mezz'ora per scrivere il codice in c, compilarlo in una libreria dinamica.
    • 10 minuti per scrivere un codice di prova in Python per verificare se c codice funziona.
    • Circa un'ora di fare alcuni test e riordinare il c codice.
    • Quindi ho inserito il ccodice nella base di codice reale e ho visto che ctypesnon funziona benemultiprocessing modulo poiché il suo gestore non è selezionabile per impostazione predefinita.
    • Circa 20 minuti ho riordinato il mio codice per non utilizzarlo multiprocessing modulo e riprovato.
    • Quindi seconda funzione nel mio c codice ha generato segfault nella mia base di codice anche se ha superato il mio codice di prova. Bene, questa è probabilmente colpa mia per non aver verificato bene i casi limite, stavo cercando una soluzione rapida.
    • Per circa 40 minuti ho cercato di determinare le possibili cause di questi segfault.
    • Ho diviso le mie funzioni in due librerie e riprovato. Aveva ancora segfault per la mia seconda funzione.
    • Ho deciso di lasciare andare la seconda funzione e usare solo la prima funzione del ccodice e alla seconda o terza iterazione del loop python che lo utilizza, avevo un problema UnicodeErrordi non decodificare un byte in qualche posizione anche se ho codificato e decodificato tutto esplicitamente.

A questo punto, ho deciso di cercare un'alternativa e ho deciso di esaminare cython:

  • Cython
    • 10 minuti di lettura del mondo di cython hello .
    • 15 minuti di controllo SO su come utilizzare cython setuptoolsinvece di distutils.
    • 10 minuti di lettura sui tipi di cython e sui tipi di pitone. Ho imparato che posso usare la maggior parte dei tipi di pitone integrati per la tipizzazione statica.
    • 15 minuti di riannotazione del mio codice Python con tipi di cython.
    • 10 minuti di modifica di my setup.pyper utilizzare il modulo compilato nella mia base di codice.
    • Collegato il modulo direttamente alla multiprocessingversione di codebase. Funziona.

Per la cronaca, ovviamente, non ho misurato i tempi esatti del mio investimento. Potrebbe benissimo essere che la mia percezione del tempo fosse un po 'troppo attenta a causa dello sforzo mentale richiesto mentre avevo a che fare con i tipi. Ma dovrebbe trasmettere la sensazione di affrontare cythonectypes

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.