Python Languagectypes

Вступление

ctypes - это встроенная библиотека python, которая вызывает экспортированные функции из встроенных библиотек.

Примечание. Поскольку эта библиотека обрабатывает скомпилированный код, она зависит от ОС.

Основное использование

Предположим, мы хотим использовать функцию ntohl libc .

Во-первых, мы должны загрузить libc.so :

>>> from ctypes import *
>>> libc = cdll.LoadLibrary('libc.so.6')
>>> libc
<CDLL 'libc.so.6', handle baadf00d at 0xdeadbeef>

Затем мы получаем объект функции:

>>> ntohl = libc.ntohl
>>> ntohl
<_FuncPtr object at 0xbaadf00d>

И теперь мы можем просто вызвать функцию:

>>> ntohl(0x6C)
1811939328
>>> hex(_)
'0x6c000000'

Что делает именно то, что мы ожидаем от этого.

Обычные подводные камни

Не удалось загрузить файл

Первой возможной ошибкой является невозможность загрузить библиотеку. В этом случае OSError обычно поднимается.

Это происходит потому, что файл не существует (или не может быть найден ОС):

>>> cdll.LoadLibrary("foobar.so")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 425, in LoadLibrary
    return self._dlltype(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 347, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: foobar.so: cannot open shared object file: No such file or directory

Как вы можете видеть, ошибка ясна и довольно показательна.

Вторая причина заключается в том, что файл найден, но не соответствует формату.

>>> cdll.LoadLibrary("libc.so")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 425, in LoadLibrary
    return self._dlltype(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 347, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: /usr/lib/i386-linux-gnu/libc.so: invalid ELF header

В этом случае файл является файлом сценария, а не файлом .so . Это также может произойти при попытке открыть .dll файл на машине Linux или 64-битном файле на 32-битном интерпретаторе python. Как вы можете видеть, в этом случае ошибка немного более расплывчата и требует некоторого рытья.

Отсутствие доступа к функции

Предположим, что мы успешно загрузили файл .so , тогда нам нужно получить доступ к нашей функции, как это было сделано в первом примере.

Когда используется несуществующая функция, возникает AttributeError :

>>> libc.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 360, in __getattr__
    func = self.__getitem__(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 365, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /lib/i386-linux-gnu/libc.so.6: undefined symbol: foo

Основной объект ctypes

Самый основной объект - это int:

>>> obj = ctypes.c_int(12)
>>> obj
c_long(12)

Теперь obj ссылается на кусок памяти, содержащий значение 12.

Доступ к этому значению можно получить напрямую и даже изменить:

>>> obj.value
12
>>> obj.value = 13
>>> obj
c_long(13)

Поскольку obj относится к куску памяти, мы также можем узнать его размер и местоположение:

>>> sizeof(obj)
4
>>> hex(addressof(obj))
'0xdeadbeef'

массивы ctypes

Как знает любой хороший программист C, одно значение не дойдет до вас. Что действительно вызовет нас, это массивы!

>>> c_int * 16
<class '__main__.c_long_Array_16'>

Это не реальный массив, но это довольно чертовски близко! Мы создали класс, который обозначает массив из 16 int s.

Теперь все, что нам нужно сделать, это инициализировать его:

>>> arr = (c_int * 16)(*range(16))
>>> arr
<__main__.c_long_Array_16 object at 0xbaddcafe>

Теперь arr является фактическим массивом, который содержит числа от 0 до 15.

Доступ к ним возможен, как и любой список:

>>> arr[5]
5
>>> arr[5] = 20
>>> arr[5]
20

И как и любой другой объект ctypes , он также имеет размер и местоположение:

>>> sizeof(arr)
64 # sizeof(c_int) * 16
>>> hex(addressof(arr))
'0xc000l0ff'

Функции упаковки для ctypes

В некоторых случаях функция C принимает указатель на функцию. Как пользователи ctypes , мы хотели бы использовать эти функции и даже передавать функцию python в качестве аргументов.

Определим функцию:

>>> def max(x, y):
        return x if x >= y else y

Теперь эта функция принимает два аргумента и возвращает результат одного и того же типа. Для примера предположим, что тип - это int.

Как и в примере массива, мы можем определить объект, который обозначает этот прототип:

>>> CFUNCTYPE(c_int, c_int, c_int)
<CFunctionType object at 0xdeadbeef>

Этот прототип обозначает функцию, которая возвращает c_int (первый аргумент) и принимает два аргумента c_int (другие аргументы).

Теперь давайте обернем функцию:

>>> CFUNCTYPE(c_int, c_int, c_int)(max)
<CFunctionType object at 0xdeadbeef>

Прототипы функций имеют больше возможностей: они могут обернуть функцию ctypes (например, libc.ntohl ) и убедиться, что при вызове функции используются правильные аргументы.

>>> libc.ntohl() # garbage in - garbage out
>>> CFUNCTYPE(c_int, c_int)(libc.ntohl)()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: this function takes at least 1 argument (0 given)

Комплексное использование

Давайте объединим все приведенные выше примеры в один сложный сценарий: используя функцию lfind libc .

Подробнее о функции читайте на странице руководства . Я настоятельно рекомендую вам прочитать его перед продолжением.

Сначала мы определим правильные прототипы:

>>> compar_proto = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
>>> lfind_proto = CFUNCTYPE(c_void_p, c_void_p, c_void_p, POINTER(c_uint), c_uint, compar_proto)

Затем создадим переменные:

>>> key = c_int(12)
>>> arr = (c_int * 16)(*range(16))
>>> nmemb = c_uint(16)

И теперь мы определяем функцию сравнения:

>>> def compar(x, y):
        return x.contents.value - y.contents.value

Обратите внимание, что x и y являются POINTER(c_int) , поэтому нам нужно разыменовать их и принять их значения, чтобы фактически сравнить значение, хранящееся в памяти.

Теперь мы можем объединить все вместе:

>>> lfind = lfind_proto(libc.lfind)
>>> ptr = lfind(byref(key), byref(arr), byref(nmemb), sizeof(c_int), compar_proto(compar))

ptr - возвращаемый указатель пустоты. Если key не был найден в arr , значение будет None , но в этом случае мы получили действительное значение.

Теперь мы можем преобразовать его и получить доступ к значению:

>>> cast(ptr, POINTER(c_int)).contents
c_long(12)

Кроме того, мы видим, что ptr указывает на правильное значение внутри arr :

>>> addressof(arr) + 12 * sizeof(c_int) == ptr
True