DLL & Python
Недавно меня заинтересовала тема использования 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 Passport{
char* name;
char* surname;
int var;
};
void DLL_EXPORT SetName(char* new_name);
void DLL_EXPORT SetSurname(char* new_surname);
void DLL_EXPORT SetPassport(Passport* new_passport);
void DLL_EXPORT GetPassport(void);
#ifdef __cplusplus
}
#endif
#endif // __MAIN_H__
В коде main.h
определена структура Passport с тремя полями: два указателя и целочисленная переменная. Кроме того, четыре функции объявлены, как экспортируемые.
Код DLL на С:
main.c
#include "main.h"
#define SIZE_BUF 20
struct Passport passport; // объявляем переменную passport типа Passport
// Функция установки имени
void DLL_EXPORT SetName(char* new_name)
{
printf("SetName\n");
strcpy(passport.name, new_name);
}
// Функция установки фамилии
void DLL_EXPORT SetSurname(char* new_surname)
{
printf("SetSurname\n");
strcpy(passport.surname, new_surname);
}
// Функция установки полей структуры.
// На вход принимает указатель на структуру
void DLL_EXPORT SetPassport(Passport* new_passport)
{
printf("SetPassport\n");
strcpy(passport.name, new_passport->name);
strcpy(passport.surname, new_passport->surname);
passport.var = new_passport->var;
}
// Вывести в консоль данные структуры
void DLL_EXPORT GetPassport(void)
{
printf("GetPassport: %s | %s | %d\n", passport.name, passport.surname, passport.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");
passport.name = (char*)malloc(SIZE_BUF * sizeof(char)); // выделение памяти
passport.surname = (char*)malloc(SIZE_BUF * sizeof(char)); // выделение памяти
passport.var = 17; // начальная инициализация переменной
SetName("Default"); // начальная инициализация буфера имени
SetSurname("Passport"); // начальная инициализация буфера фамилии
return 1;
case DLL_PROCESS_DETACH:
free (passport.name); // Освобождение памяти
free (passport.surname); // Освобождение памяти
printf("DETACH DLL\n");
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return TRUE; // succesful
}
Внутри кейса DLL_PROCESS_ATTACH
происходит выделение памяти под строки и начальная инициализация полей структуры. Выше DllMain
определены функции:
GetPassport — вывод полей структуры
passport
в консоль.
*SetName(char new_name)** — установка поля
name
структурыpassport
.
*SetSurname(char new_surname)** — установка поля
surname
структурыpassport
.
*SetPassport(Passport new_passport)** — установка всех полей структуры
passport
. Принимает в качестве аргумента указатель на структуру с новыми полями.
Теперь можно подключить библиотеку в Python.
Код на Python
from ctypes import *
class Passport(Structure): # класс, который соответствует структуре Passport
_fields_ = [("name", c_char_p), # из файла main.h
("surname", c_char_p),
("var", c_int)]
lib_dll = cdll.LoadLibrary("DLL_example.dll") # подключаю свою DLL
lib_dll.SetPassport.argtypes = [POINTER(Passport)] # указываем, тип аргумента функции
lib_dll.GetPassport() # вывод в консоль структуры
lib_dll.SetName(c_char_p(b"Yury"))
lib_dll.SetSurname(c_char_p(b"Orlov"))
lib_dll.GetPassport() # вывод в консоль структуры
name = str.encode(("Vasiliy")) # первый вариант получения указателя на байтовую строку
surname = c_char_p((b'Pupkin')) # второй вариант получения указателя на байтовую строку
passport = Passport(name, surname, 34) # создаем объект структуры Passport
lib_dll.SetPassport(pointer(passport)) # передача структуры в функцию в DLL
lib_dll.GetPassport() # вывод в консоль структуры
В коде выше многое уже знакомо, кроме создания структуры аналогичной той, которая объявлена в DLL и передачи указателя на эту структуру из Python в DLL.
Результат:
Load DLL in Python
SetName
SetSurname
GetPassport: Default | Passport | 17
SetName
SetSurname
GetPassport: Yury | Orlov | 17
SetPassport
GetPassport: Vasiliy | Pupkin | 34
DETACH DLL
P.S: Думаю, что примеры и объяснения из статьи помогут вам быстро начать использовать DLL библиотеки из Python. Ну а если вы не смогли найти ответы на свои вопросы то может помогут ссылки ниже. Если у кого-то будут вопросы — постараюсь ответить, если будут замечания — постараюсь исправить. Спасибо, что дочитали!
Полезные ссылки:
- Документации ctypes — много примеров
- Что такое DLL? — много и полно о dll.
- [C/C++ из Python (ctypes)](C/C++ из Python (ctypes)) — хорошая статья.
- Передача двумерных списков из python в DLL
- Связывание исполняемого файла с библиотекой DLL
- Github с dll "Hello-world" и её кодом — можно воспользоваться для тестирования.
- Что такое TCHAR, WCHAR, LPSTR, LPWSTR,LPCTSTR — о типах данных.
- Динамические библиотеки для гурманов — статья с большим количеством интересных нюансов.