Jak stworzyć wrapper Pythona dla bibliotek C i C++
Nauczysz się tutaj tworzyć i implementować klasę opakowującą dla wywoływania funkcji C i C++ bezpośrednio w Pythonie. Będziemy używać wbudowanego modułu o nazwie ctypes. Zgodnie z oficjalną dokumentacją ctypes to:
biblioteka funkcji obcych dla Pythona. Zapewnia typy danych zgodne z C i umożliwia wywoływanie funkcji w bibliotekach DLL lub bibliotekach współdzielonych. Może być używana do opakowania tych bibliotek w czysty Python
Artykuł ten składa się z trzech sekcji:
- Konfiguracja
- Implementacja
- Podsumowanie
Przejdźmy zatem do konfiguracji potrzebnych nam materiałów.
Konfiguracja
Ja będę używać Pythona 3.7.4. Można śmiało korzystać z innych wersji, o ile jest w nich moduł ctypes
. Mocno zalecam utworzenie wirtualnego środowiska dla tego projektu.
Upewnij się, że masz bibliotekę dll
, która jest gotowa do użycia przez nasz plik Python. Potrzebujesz tylko pliku dll
do utworzenia wrappera. Plik nagłówkowy jest opcjonalny, jeśli masz dostęp i znasz dostępne funkcje i parametry. Jeśli korzystasz z zewnętrznych bibliotek, upewnij się, że masz pliki nagłówkowe, dzięki czemu będzie łatwiej napisać właściwy wrapper.
Utwórz nowy plik Pythona w tym samym katalogu, co dll
. Ja nazwę go add_wrapper.py
, a Ty nazwij go, jak chcesz.
Napiszmy teraz kod w Pythonie.
Implementacja
Import
Zaimportuj następujące moduły w górnej części swojego pliku:
from ctypes import *
from sys import platform
ctypes
— jest to podstawowy moduł do ładowania bibliotek współdzielonych i wywoływania znajdujących się w nich funkcji.sys
— Będziemy używać tego modułu do określenia systemu operacyjnego i załadowania odpowiedniej biblioteki.
Ładowanie biblioteki
Zakładając, że masz osobne biblioteki dla systemów operacyjnych Windows i Linux, dodaj następujący kod, aby załadować odpowiednią bibliotekę dla swojego systemu.
shared_lib_path = "./add.so"
if platform.startswith('win32'):
shared_lib_path = "./add.dll"
try:
add_lib = CDLL(shared_lib_path)
print("Successfully loaded ", add_lib)
except Exception as e:
print(e)
Powyższy kod przechował referencję do biblioteki w zmiennej o nazwie add_lib
. Użyjemy jej do wywołania dostępnych funkcji później.
Po uruchomieniu pliku Python powinniśmy zobaczyć takie dane:
<CDLL './add.dll', handle ... at ...>
Nagłówek
Rzućmy okiem na nagłówek biblioteki DLL.
#ifndef _ADD_H
#define _ADD_H
extern "C"
{
__declspec(dllexport) int add(int, int);
__declspec(dllexport) int sub(int, int);
__declspec(dllexport) int mul(int, int);
}
#endif
Jest on zakodowany w C++ i dostępne są trzy funkcje. Wszystkie funkcje akceptują dwa parametry i zwracają zmienną całkowitą. W prawdziwym przypadku jest to o wiele bardziej skomplikowane, ze złożonymi typami danych i wskaźnikami.
Typy danych
Zanim przejdziemy dalej, przyjrzyjmy się dostępnym typom danych, udostępnianym przez ctypes, który reprezentuje ten sam typ danych w C.
Obraz — Python
Możesz zainicjować typ danych, tak jak pokazano poniżej:
a = c_int()
Po wydrukowaniu otrzymasz następujący wynik, który wskazuje, że jest to long i ma wartość 0
.
c_long(0)
Nie zdziw się, jeśli wynikiem nie jest int
. Na podstawie oficjalnej dokumentacji:
Na platformach, na których
sizeof(long) == sizeof(int)
, jest to alias doc_long
. Nie powinno Cię dziwić, jeśli zostanie wydrukowanec_long
, tam gdzie spodziewasz sięc_int
— to w rzeczywistości jedno i to samo.
Możesz przekazać parametr, który służy jako wartość początkowa.
a = c_int(25)
Aby uzyskać rzeczywistą wartość, musisz użyć atrybutu value
.
print(a.value)
Wynikiem powinno być 25
.
W przypadku specjalnych typów danych, takich jak char kończący się nullem (który reprezentuje ciąg znaków w Pythonie), musisz użyć następującego kodu. Taka składnia zostanie dokładniej omówiona w dalszej części artykułu.
serialNumber = c_char * 13
Funkcje
Wykorzystamy zmienną add_lib, którą zdefiniowaliśmy wcześniej, do wywołania odpowiednich funkcji. Pamiętaj, że mamy dostępne tylko trzy funkcje, co wiemy z nagłówka:
add
sub
mul
add_lib.add(2, 5)
Nazwa funkcji i typy danych funkcji wejściowej powinny być dokładnie takie same, jak w nagłówku. Jeśli spróbujesz wywołać niedostępną funkcję, zobaczysz następujący błąd:
AttributeError: function 'test' not found
Użycie niewłaściwego typu danych spowoduje następujący błąd:
ctypes.ArgumentError: argument 1 : <class: TypeError>
Niektórych typów danych, takich jak int, można używać bezpośrednio, bez konieczności deklarowania ich jako typów danych ctypes. Mocno polecam jednak to robić.
a = c_int(25)
b = c_int(10)
add_lib.sub(a, b)
Wynikiem powinno być 15
.
Typ zwracany
Domyślnie, jako typ danych zwracana jest wartość int. Musisz ręcznie zdefiniować typ zwracany za pomocą atrybutu restype
.
result = add_lib.mul
result.restype = c_int
result(a, b)
Należy pamiętać, że użycie niewłaściwego typu wpłynie na wartość w nim zawartą. Musisz zastosować ten sam typ danych, co w nagłówku. Jeśli zamierzasz uzyskać z niego inne typy danych, po prostu zrzutuj je po uzyskaniu wyniku.
Wskaźnik
Możesz utworzyć nowy wskaźnik, używając następującego kodu:
temperature = c_int(25)
temperature_p = pointer(temperature)
Musisz użyć atrybutu contents
, aby uzyskać dostęp do treści, która kryje się pod wskaźnikiem.
temperature_p.contents
Powinniśmy otrzymać c_long(25)
jako wartość wyjściową. Możesz połączyć ją z atrybutem value
, aby uzyskać dostęp do właściwej wartości.
temperature_p.contents.value
Jeśli nie potrzebujesz jednak wskaźnika i chcesz go użyć jako parametr funkcji, ctypes zapewnia funkcję byref, która służy do przekazywania parametrów przez referencję. Poniższy kod pokazuje, jak to działa (i nie dotyczy biblioteki add_lib
):
temperature = c_int(25)
result = example_lib.calc_temp(byref(temperature))
Struktury i unie
Kiedy masz do czynienia ze strukturą, musisz użyć następującego kodu. Upewnij się, że typ danych jest zgodny z oryginalnymi danymi zdefiniowanymi w nagłówku.
class lib_sdk_info(Structure):
_fields_ = [("sdk_version_major", c_uint8),
("sdk_version_minor", c_uint8),
("sdk_build_major", c_uint8),
("sdk_build_minor", c_uint8)]
Możesz stworzyć nową strukturę, używając poniższego kodu:
info = lib_sdk_info()
Dostęp do pól można uzyskać w następujący sposób.
info.sdk_build_minor
Jeśli zajdzie potrzeba wydrukowania wszystkich informacji wewnątrz struktury, zaleca się wykonanie tego za pomocą następującej pętli for
:
for field_name, field_type in info._fields_:
print(field_name, getattr(info, field_name))
Tablica
Najlepszym sposobem na utworzenie tablicy jest pomnożenie typu danych przez liczbę całkowitą. Możesz też w razie potrzeby utworzyć tablicę struktur.
phoneArray = c_int * 10
Możesz następnie utworzyć nowe wystąpienie tablicy i używać jej tak, jakby była to lista.
phone_list = phoneArray()
phone_list[0] = 32
for i in phone_list:
print(i)
Podsumowanie
Podsumujmy to, czego się dzisiaj nauczyliśmy.
Zaczęliśmy od krótkiego objaśnienia ctypes i skonfigurowaliśmy wymaganą bibliotekę dll, abyśmy mogli jej używać. Następnie załadowaliśmy współdzielone biblioteki, tak aby pasowały do używanego systemu operacyjnego, i dogłębnie zbadaliśmy dostępne typy danych dostarczane przez moduł. Potem wywołaliśmy funkcje dostępne z biblioteki dll na podstawie danych z nagłówka.
Przetestowaliśmy również tworzenie wskaźnika i wysłaliśmy go do funkcji za pomocą byref. Ponadto stworzyliśmy odpowiedni obiekt Structure oraz Array, których można normalnie używać w Pythonie.
Dziękuję za uwagę!
Źródła
Oryginał tekstu w języku angielskim możesz przeczytać tutaj.