DLL & Python
- понедельник, 27 апреля 2020 г. в 00:26:25
Недавно меня заинтересовала тема использования DLL из Python. Кроме того было интересно разобраться в их структуре, на тот случай, если придется менять исходники библиотек. После изучения различных ресурсов и примеров на эту тему, стало понятно, что применение динамических библиотек может сильно расширить возможности Python. Собственные цели были достигнуты, а чтобы опыт не был забыт, я решил подвести итог в виде статьи — структурировать свой знания и полезные источники, а заодно ещё лучше разобраться в данной теме.
Под катом вас ожидает статья с различными примерами, исходниками и пояснениями к ним.
Надеюсь из содержания немного станет понятнее какую часть нужно открыть, чтобы найти ответы на свои вопросы.
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
уже рассмотрен выше, теперь о двух других:
О явной и неявной компоновке можно прочесть подробно в статье: Связывание исполняемого файла с библиотекой DLL.
В предложенном на рисунке 1 шаблоне есть функция SomeFunction
, которая может быть экспортирована из динамической библиотеки. Для того, чтобы это показать, при объявлении функции указывается __declspec(dllexport)
. Например, так:
#define DLL_EXPORT __declspec(dllexport)
void DLL_EXPORT SomeFunction(const LPCSTR sometext);
Функции, не объявленные таким образом, нельзя будет вызывать снаружи.
Первым делом, расскажу, как подключать уже собранные DLL, затем, как вызывать из них функции и передавать аргументы, а уже после этого, постепенно доделаю шаблон из Code Blocks и приведу примеры работы с собственной DLL.
Основной библиотекой в Python для работы с типами данных, совместимыми с типами языка С является ctypes
. В документации на ctypes представлено много примеров, которым стоит уделить внимание.
Чтобы начать работать с DLL, необходимо подключить библиотеку к программе на Python. Сделать это можно тремя способами:
Про соглашения о вызове функций.
Для первого примера будем использовать стандартную 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 нужно передавать простые строки или цифры. Бывают случаи, когда требуется передавать указатели на строки, переменные или структуры. Кроме того, функции могут и возвращать структуры, указатели и много другое.
Модуль 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 уже был рассмотрен выше, а сейчас, когда дело дошло до написания своей 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.
Чтобы подвести итог по использованию 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. Ну а если вы не смогли найти ответы на свои вопросы то может помогут ссылки ниже. Если у кого-то будут вопросы — постараюсь ответить, если будут замечания — постараюсь исправить. Спасибо, что дочитали!