python

DLL & Python

  • понедельник, 27 апреля 2020 г. в 00:26:25
https://habr.com/ru/post/499152/
  • Python
  • Программирование
  • C


DLL & Python


image


Недавно меня заинтересовала тема использования DLL из Python. Кроме того было интересно разобраться в их структуре, на тот случай, если придется менять исходники библиотек. После изучения различных ресурсов и примеров на эту тему, стало понятно, что применение динамических библиотек может сильно расширить возможности Python. Собственные цели были достигнуты, а чтобы опыт не был забыт, я решил подвести итог в виде статьи — структурировать свой знания и полезные источники, а заодно ещё лучше разобраться в данной теме.


Под катом вас ожидает статья с различными примерами, исходниками и пояснениями к ним.


Содержание


  • Структура DLL
  • DLL & Python
    • Подключение DLL
    • Типы данных в С и Python
    • Аргументы функция и возвращаемые значения
    • Своя DLL и ее использование
  • Полезные ссылки:

Надеюсь из содержания немного станет понятнее какую часть нужно открыть, чтобы найти ответы на свои вопросы.


Структура DLL


DLL — Dynamic Link Library — динамическая подключаемая библиотека в операционной системе (ОС) Windows. Динамические библиотеки позволяют сделать архитектуру более модульной, уменьшить количество используемых ресурсов и упрощают модификацию системы. Основное отличие от .EXE файлов — функции, содержащиеся в DLL можно использовать по одной.


Учитывая, что статья не о самих библиотеках, лучше просто оставить здесь ссылку на довольно информативную статью от Microsoft: Что такое DLL?.




Для того, чтобы понять, как использовать динамические библиотеки, нужно вникнуть в их структуру.


DLL содержит набор различных функций, которые потом можно использовать по-отдельности. Но также есть возможность дополнительно указать функцию точки входа в библиотеку. Такая функция имеет обязательное имя DllMain и вызывается, когда процессы или потоки прикрепляются к DLL или отделяются от неё. Это можно использовать для инициализации различных структур данных или их уничтожения.



Рисунок 1 — Пустой template, предлагаемый Code Blocks для проекта DLL.


На рисунке 1 приведен шаблон, который предлагает Code Blocks, при выборе проекта типа DLL. В представленном шаблоне есть две функции:


#define DLL_EXPORT __declspec(dllexport) // обязательно определять функции, 
                                         // которые могут быть экспортированы из                                                 // библиотеки
void DLL_EXPORT SomeFunction(const LPCSTR sometext); // просто функция для примера, она вызывает вывод сообщения в окно

extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) //функция точки входа

Для начала стоит подробнее рассмотреть функциюDllMain. Через нее ОС может уведомлять библиотеку о нескольких событиях (fdwReason):


  • DLL_PROCESS_ATTACH – подключение DLL. Процесс проецирования DLL на адресное пространство процесса. С этим значением DllMain вызывается всякий раз, когда какой-то процесс загружает библиотеку с явной или неявной компоновкой.


  • DLL_PROCESS_DETACH – отключение DLL от адресного пространства процесса. С этим значением DllMain вызывается при отключении библиотеки.


  • DLL_THREAD_ATTACH – создание процессом, подключившим DLL, нового потока. Зачем DLL знать о каких-то там потоках? А вот зачем, далеко не каждая динамическая библиотека может работать в многопоточной среде.


  • DLL_THREAD_DETACH – завершение потока, созданного процессом, подключившим DLL. Если динамическая библиотека создает для каждого потока свои "персональные" ресурсы (локальные переменные и буфера), то это уведомление позволяет их своевременно освобождать.



Опять же, в тему структуры DLL можно углубляться до бесконечности, там есть много различных нюансов, о которых немного изложено в этой статье.




У DllMain не так много аргументов, самый важный fdwReason уже рассмотрен выше, теперь о двух других:


  • Аргумент lpvReserved указывает на способ подключения DLL:
    • 0 — библиотека загружена с явной компоновкой.
    • 1 — библиотека загружена с неявной компоновкой.
  • Аргумент hinstDLL содержит описатель экземпляра DLL. Любому EXE- или DLL-модулю, загружаемому в адресное пространство процесса, присваивается уникальный описатель экземпляра.

О явной и неявной компоновке можно прочесть подробно в статье: Связывание исполняемого файла с библиотекой DLL.




В предложенном на рисунке 1 шаблоне есть функция SomeFunction, которая может быть экспортирована из динамической библиотеки. Для того, чтобы это показать, при объявлении функции указывается __declspec(dllexport). Например, так:


#define DLL_EXPORT __declspec(dllexport) 
void DLL_EXPORT SomeFunction(const LPCSTR sometext);

Функции, не объявленные таким образом, нельзя будет вызывать снаружи.


DLL & Python


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


Подключение DLL


Основной библиотекой в Python для работы с типами данных, совместимыми с типами языка С является ctypes. В документации на ctypes представлено много примеров, которым стоит уделить внимание.


Чтобы начать работать с DLL, необходимо подключить библиотеку к программе на Python. Сделать это можно тремя способами:


  • cdll — загружает динамическую библиотеку и возвращает объект, а для использования функций DLL нужно будет просто обращаться к атрибутам этого объекта. Использует соглашение вызовов cdecl.
  • windll — использует соглашение вызовов stdcall. В остальном идентична cdll.
  • oledll — использует соглашение вызовов stdcall и предполагается, что функции возвращают код ошибки Windows HRESULT. Код ошибки используется для автоматического вызова исключения WindowsError.

Про соглашения о вызове функций.


Для первого примера будем использовать стандартную Windows DLL библиотеку, которая содержит всем известную функцию языка С — printf(). Библиотека msvcrt.dll находится в папке C:\WINDOWS\System32.


Код Python:


from ctypes import *

lib = cdll.msvcrt   # подключаем библиотеку msvcrt.dll
lib.printf(b"From dll with love!\n") # вывод строки через стандартную printf

var_a = 31
lib.printf(b"Print int_a = %d\n", var_a) # вывод переменной int 
                                # printf("Print int_a = %d\n", var_a); // аналог в С

Результат:


From dll with love!
Print int_a = 31

Можно использовать подключение библиотеки с помощью метода windll либо oledll, для данного кода разницы не будет, вывод не изменится.


Если речь не идет о стандартной библиотеке, то конечно следует использовать вызов с указанием пути на dll. В ctypes для загрузки библиотек предусмотрен метод LoadLibrary. Но есть еще более эффективный конструктор CDLL, он заменяет конструкцию cdll.LoadLibrary. В общем, ниже показано два примера вызова одной и той же библиотеки msvcrt.dll.


Код Python:


from ctypes import *

lib = cdll.LoadLibrary(r"C:\Windows\System32\msvcrt.dll")   
lib.printf(b"From dll with love!\n")    # вывод строки через стандартную printf

lib_2 = CDLL(r"C:\Windows\System32\msvcrt.dll") # подключаем библиотеку msvcrt.dll

var_a = 31
lib_2.printf(b"Print int_a = %d\n", var_a)  # вывод переменной int

Иногда случается, что необходимо получить доступ к функции или атрибуту DLL, имя которого Python не "примет"… ну бывает. На этот случай имеется функции getattr(lib, attr_name). Данная функция принимает два аргумента: объект библиотеки и имя атрибута, а возвращает объект атрибута.


Код Python:


from ctypes import *

lib = cdll.LoadLibrary(r"C:\Windows\System32\msvcrt.dll")   

var_c = 51
print_from_C = getattr(lib, "printf")  # да, тут можно вписать даже "??2@YAPAXI@Z"
print_from_C(b"Print int_c = %d\n", var_c)

Результат:


Print int_c = 51

Теперь становится понятно, как подключить библиотеку и использовать функции. Однако, не всегда в DLL нужно передавать простые строки или цифры. Бывают случаи, когда требуется передавать указатели на строки, переменные или структуры. Кроме того, функции могут и возвращать структуры, указатели и много другое.


Типы данных в С и Python


Модуль ctypes предоставляет возможность использовать типы данных совместимые с типами в языке С. Ниже приведена таблица соответствия типов данных.


Сtypes type C type Python type
c_bool _Bool bool (1)
c_char char 1-character string
c_wchar wchar_t 1-character unicode string
c_byte char int/long
c_ubyte unsigned char int/long
c_short short int/long
c_ushort unsigned short int/long
c_int int int/long
c_uint unsigned int int/long
c_long long int/long
c_ulong unsigned long int/long
c_longlong __int64 or long long int/long
c_ulonglong unsigned __int64 or unsigned long long int/long
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL terminated) string or None
c_wchar_p wchar_t * (NUL terminated) unicode or None
c_void_p void * int/long or None

Таблица 1 — Соответствие типов данных языка Python и языка C, которое предоставляет модуль ctypes.


Первое, что стоит попробовать — это использовать указатели, куда без них? Давайте напишем программу, где создадим строку и указатель на неё, а потом вызовем printf() для них:


Код:


from ctypes import *

lib = CDLL(r"C:\Windows\System32\msvcrt.dll")
printf = lib.printf # объект функции printf()

int_var = c_int(17) # переменная типа int из C
printf(b"int_var = %d\n", int_var)

str_ = b"Hello, World\n" # строка в Python
str_pt = c_char_p(str_)  # указатель на строку
printf(str_pt)           

print(str_pt)            
print(str_pt.value) # str_pt - указатель на строку, значение можно получить с использованием атрибута value

Результат:


int_var = 17
Hello, World
c_char_p(2814054827168)
b'Hello, World\n'

Если вы создали указатель, то разыменовать (получить доступ к значению, на которое он указывает) можно с использованием атрибута value, пример выше.


Аргументы функций и возвращаемые значения


По умолчанию предполагается, что любая экспортируемая функция из динамической библиотеки возвращает тип int. Другие возвращаемые типы можно указать при помощи атрибута restype. При этом, чтобы указать типы аргументов функции можно воспользоваться атрибутом argtypes.


Например, стандартная функция strcat принимает два указателя на строки и возвращает один указатель на новую строку. Давайте попробуем ей воспользоваться.


char *strcat (char *destination, const char *append); // C функция для конкатонации (склеивания) строк 

Код Python:


from ctypes import *

libc = CDLL(r"C:\Windows\System32\msvcrt.dll")

strcat = libc.strcat        # получаем объект функции strcat
strcat.restype = c_char_p   # показываем, что функция будет возвращать указатель на                                 # строку
strcat.argtypes = [c_char_p, c_char_p] # показывает типы аргументов функции

str_1 = b"Hello,"
str_2 = b" Habr!"
str_pt = strcat(str_1, str_2) # вызываем стандартную функцию

print(str_pt)

Результат:


b'Hello, Habr!'

На этом закончим с примерами использования готовых DLL. Давайте попробуем применить знания о структуре DLL и модуле ctypes для того, чтобы собрать и начать использовать собственную библиотеку.


Своя DLL и ее использование


Пример 1


Шаблон DLL уже был рассмотрен выше, а сейчас, когда дело дошло до написания своей DLL и работы с ней, выскочили первые и очевидные грабли — несовместимость разрядности DLL и Python. У меня на ПК установлен Python x64, оказалось, что как бы не были DLL универсальны, разрядность DLL должна соответствовать разрядности Python. То есть, либо ставить компилятор x64 и Python x64, либо и то и то x32. Хорошо, что это не сложно сделать.


Ниже привожу код шаблона DLL, в который добавил вывод строки при подключении библиотеки, а также небольшой разбор и вывод аргументов, с которыми вызвалась DllMain. В примере можно понаблюдать, какие участки кода библиотеки вызываются и когда это происходит.


Код DLL на С:


// a sample exported function
void __declspec(dllexport) SomeFunction(const LPCSTR sometext)
{
    MessageBoxA(0, sometext, "DLL Message", MB_OK | MB_ICONINFORMATION);
}

extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            printf("Load DLL in Python\n");

            printf("HINSTANCE = %p\n",hinstDLL); // Вывод описателя экземпляра DLL

            if (lpvReserved)                     // Определение способа загрузки
              printf("DLL loaded with implicit layout\n"); 
            else
              printf("DLL loaded with explicit layout\n");          
            return 1;                            // Успешная инициализация

        case DLL_PROCESS_DETACH:
            printf("DETACH DLL\n");
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;
    }
    return TRUE; // succesful
}

Код Python:


from ctypes import *

lib_dll = cdll.LoadLibrary("DLL_example.dll")   # подключаю свою DLL
str_ = b'Hello, Habr!'
p_str = c_char_p(str_)                          # получаю указатель на строку str_
lib_dll.SomeFunction(p_str)                     # вызываю SomeFunction из DLL

Функция SomeFunction получает указатель на строку и выводит её в окно. На рисунке ниже показана работа программы.



Рисунок 2 — Демонстрация работы шаблона библиотеки из Code Blocks.


Все действия происходящие в кейсе DLL_PROCESS_ATTACH, код которого приведен ниже, вызываются лишь одной строкой в Python коде:


lib_dll = cdll.LoadLibrary("DLL_example.dll")   # подключение библиотеки


Рисунок 3 — Действия происходящие при подключении DLL.


Пример 2


Чтобы подвести итог по использованию DLL библиотек из Python, приведу пример, в котором есть начальная инициализация параметров и передача новых через указатели на строки и структуры данных. Этот код дает понять, как написать аналог структуры С в Python. Ниже привожу код main.c, man.h и main.py.


Код DLL на С:


main.h


#ifndef __MAIN_H__
#define __MAIN_H__

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <malloc.h>

#define DLL_EXPORT __declspec(dllexport) // обязательно определять функции, 
                                         // которые могут быть экспортированы из                                                 // библиотеки
#ifdef __cplusplus
extern "C"
{
#endif

struct Pasport{
    char*  name;
    char*  sorname;
    int var;
};

void DLL_EXPORT SetName(char* new_name);
void DLL_EXPORT SetSorname(char* new_sorname);
void DLL_EXPORT SetPasport(Pasport* new_pasport);
void DLL_EXPORT GetPasport(void);

#ifdef __cplusplus
}
#endif
#endif // __MAIN_H__

В коде main.h определена структура Pasport с тремя полями: два указателя и целочисленная переменная. Кроме того, четыре функции объявлены, как экспортируемые.


Код DLL на С:


main.c


#include "main.h"

#define SIZE_BUF 20

struct Pasport pasport; // объявляем переменную pasport типа Pasport

// Функция установки имени
void DLL_EXPORT SetName(char* new_name)
{
    printf("SetName\n");
    strcpy(pasport.name, new_name);
}

// Функция установки фамилии
void DLL_EXPORT SetSorname(char* new_sorname)
{
    printf("SetSorname\n");
    strcpy(pasport.sorname, new_sorname);
}

// Функция установки полей структуры.
// На вход принимает указатель на структуру
void DLL_EXPORT SetPasport(Pasport* new_pasport)
{
    printf("SetPasport\n");
    strcpy(pasport.name, new_pasport->name);
    strcpy(pasport.sorname, new_pasport->sorname);
    pasport.var = new_pasport->var;
}

// Вывести в консоль данные структуры
void DLL_EXPORT GetPasport(void)
{
    printf("GetPasport: %s | %s | %d\n", pasport.name, pasport.sorname, pasport.var);
}

extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            printf("Load DLL in Python\n");

            pasport.name = (char*)malloc(SIZE_BUF * sizeof(char)); // выделение памяти
            pasport.sorname = (char*)malloc(SIZE_BUF * sizeof(char)); // выделение памяти

            pasport.var = 17; // начальна инициализация переменной
            SetName("Default"); // начальна инициализация буфера имени
            SetSorname("Pasport"); // начальна инициализация буфера фамилии
            return 1;

        case DLL_PROCESS_DETACH:
            free (pasport.name); // Освобождение памяти
            free (pasport.sorname); // Освобождение памяти
            printf("DETACH DLL\n");
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;
    }
    return TRUE; // succesful
}

Внутри кейса DLL_PROCESS_ATTACH происходит выделение памяти под строки и начальная инициализация полей структуры. Выше DllMain определены функции:


  • GetPasport — вывод полей структуры pasport в консоль.


  • *SetName(char new_name)** — установка поля name структуры pasport.


  • *SetSorname(char new_sorname)** — установка поля sorname структуры pasport.


  • *SetPasport(Pasport new_pasport)** — установка всех полей структуры pasport. Принимает в качестве аргумента указатель на структуру с новыми полями.



Теперь можно подключить библиотеку в Python.


Код на Python


from ctypes import *

class Pasport(Structure):              # класс, который соответствует структуре Pasport 
    _fields_ = [("name", c_char_p),    # из файла main.h
                ("sorname", c_char_p),
                ("var", c_int)]

lib_dll = cdll.LoadLibrary("DLL_example.dll")   # подключаю свою DLL
lib_dll.SetPasport.argtypes  = [POINTER(Pasport)] # указываем, тип аргумента функции

lib_dll.GetPasport()          # вывод в консоль структуры

lib_dll.SetName(c_char_p(b"Yury"))
lib_dll.SetSorname(c_char_p(b"Orlov"))

lib_dll.GetPasport()          # вывод в консоль структуры

name = str.encode(("Vasiliy"))  # первый вариант получения указателя на байтовую строку
sorname = c_char_p((b'Pupkin')) # второй вариант получения указателя на байтовую строку

pasport = Pasport(name, sorname, 34) # создаем объект структуры Pasport

lib_dll.SetPasport(pointer(pasport)) # передача структуры в функцию в DLL

lib_dll.GetPasport()          # вывод в консоль структуры

В коде выше многое уже знакомо, кроме создания структуры аналогичной той, которая объявлена в DLL и передачи указателя на эту структуру из Python в DLL.


Результат:


Load DLL in Python
SetName
SetSorname
GetPasport: Default | Pasport | 17
SetName
SetSorname
GetPasport: Yury | Orlov | 17
SetPasport
GetPasport: Vasiliy | Pupkin | 34
DETACH DLL

P.S: Думаю, что примеры и объяснения из статьи помогут вам быстро начать использовать DLL библиотеки из Python. Ну а если вы не смогли найти ответы на свои вопросы то может помогут ссылки ниже. Если у кого-то будут вопросы — постараюсь ответить, если будут замечания — постараюсь исправить. Спасибо, что дочитали!


Полезные ссылки: