Diversity w polskim IT
Ng Wai Foong
Ng Wai FoongAI Engineer @ YOOZOO GAMES

Jak stworzyć wrapper Pythona dla bibliotek C i C++

Sprawdź, jak krok po kroku zbudować klasę opakowującą, czyli wrapper dla funkcji C i C++ w Pythonie.
4.08.20205 min
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:

  1. Konfiguracja
  2. Implementacja
  3. 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 do c_long. Nie powinno Cię dziwić, jeśli zostanie wydrukowane c_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.

<p>Loading...</p>