Windows Dynamic Link Libraries Programming. Окончание

Окончание. Начало в "КГ" N№ 13

Итак, положим, что корректно написать собственную DLL у нас (у некоторых) получилось. Теперь неплохо было бы ее попробовать в деле. Свое обещание насчет Visual Basic я не забыл, прошу заметить, но для начала сделаем это на Delphi. Как я уже говорил, способов подключения динамической библиотеки к приложению два: статический и динамический. С первым случаем все просто - объявим подпрограмму подключаемого модуля как стандартную процедуру, только с директивой external с указанием пути и имени файла DLL. В нашем случае выглядеть это будет примерно так:

procedure IntToBin(n: LongInt; s: PChar);
stdcall; external 'conv.dll'; name 'IntToBin';
procedure BinToInt(s: PChar; Var n: LongInt); stdcall; external conv.dll'; name 'BinToInt';

После чего можно будет использовать их как обычные процедуры стандартного Delphi unit-а. При таком подходе операционная система загрузит conv.dll в адресное пространство приложения еще до начала его работы. И если файла библиотеки не окажется ни в папке приложения, ни в системной папке Windows, ни, наконец, в какой-либо из прописанных в переменной path, тогда у юзера появится реальный шанс выразить свои пламенные чувства к неукротимой мастдайке при виде ее по этому поводу ругани. Естественно, приложение не соизволит запуститься даже при том условии, что подключаемые таким образом функции не будут использованы. Второй недостаток такого подхода - попросту нерациональное использование системных ресурсов (в частности памяти). Любое более или менее серьезное приложение под Windows использует куда более чем одну динамическую библиотеку с солидным количеством экспортируемых функций. Также весьма естественно, что большинство залинкованных процедур так и не будут использованы приложением (пользователем) в процессе работы, т.к. скорее всего выполняют некоторые специфические действия и именно потому были вынесены в динамический модуль. В таком случае постоянное размещение в памяти полного набора такого рода пассивных кусков кода на время выполнения приложения целесообразным не назовешь.

К счастью, выход из таких ситуаций имеется, причем весьма эффективный. Это само динамическое подключение библиотек. Сотворить сие на Delphi можно следующим образом:

Var IntToBin: Procedure(n: LongInt; s: PChar); stdcall;
Handle: Integer;
Begin
Handle:= LoadLibrary('conv.dll');
@IntToBin:=GetProcAddress(Handle, 'IntToBin');
...
FreeLibrary(Handle);
End;
До того момента, пока не будет исполнен этот код (без предпоследней строки), библиотека не будет загружена и, соответственно, не отнимет лишней памяти. После использования не забудьте ее из памяти выгрузить (как раз предпоследняя строка). Такой способ дает возможность полностью запрограммировать действия, связанные с отсутствием либо самой библиотеки на диске, либо в ней какой-то из функций. Приложение может попросить пользователя указать путь к файлу, самостоятельно предпринять попытку скачать его из Internet, вызвать систему справки или, на худой конец, дать ценный совет. В отличие от первого случая, обязательность завершения работы программы зависит не от операционной системы, а целиком от программиста. При отсутствии в момент компиляции основного приложения какой-либо библиотеки динамический способ является вообще единственно возможным. Важно обратить внимание на то, что фактически библиотека размещается в адресном пространстве приложения лишь один единственный раз, при первом вызове. При последующих обращениях к функции LoadLibrary происходит лишь увеличение ее счетчика на единицу. Соответственно "выгружающая" библиотеку процедура FreeLibrary его декрементирует. Лишь при достижении счетчиком нулевого значения происходит фактическое освобождение памяти из-под DLL.

Ну и если уж лезть в дебри, так хотя бы до середины. Пусть следующее к нашему примеру никакого отношения не имеет, однако все ж интересно разобраться с еще одним аспектом, а именно: использованием (и экспортом) глобальных переменных в динамических библиотеках. Сначала о самом главном соблазне для неопытного программиста - думать, что изменение значения переменной библиотеки из одного приложения каким-то образом будет видно из другого, использующего ту же DLL. Неправда, не будет. Каждая из программ имеет собственную копию библиотеки в своем адресном пространстве (говорим не о shared), а то есть и полный набор переменных тоже. Другое дело, когда речь идет об одном и том же приложении - здесь в этом вопросе прослеживается полная аналогия с обычными unit-ами. Честно говоря, экспорт переменных - явление довольно редкое. Объявить их фактически можно - этим библиотека ничем от стандартного модуля не отличается. Интересно становится при попытке с ними как-то работать. Напрямую Delphi не в состоянии просто реализовать возможность экспорта глобальных переменных динамической библиотеки. В большинстве случаев это мало кому нужно, однако программисты народ такой, что от "большинства случаев" стараются держаться подальше. При необходимости можно исхитриться следующим образом: экспортировать специальные функции, которые и будут заниматься присвоением и извлечением значений переменных библиотеки. В качестве примера можно привести API-функции SetCaretBlinkTime и GetCaretBlinkTime, организующие управление частотой мерцания курсора. В принципе, они как раз и занимаются организацией записи и чтения значения системной переменной.

Итак, пора заканчивать с лирическими отступлениями и возвращаться к обещанному примеру - Visual Basic. На уровне модуля объявим наши DLL-процедуры.

Public Declare Sub IntToBin Lib "conv.dll"
(ByVal n As Long, ByVal s As String)
Public Declare Sub BinToInt Lib "conv.dll"
(ByVal s As String, ByRef n As Long)

Обратите внимание на директивы ByVal при передаче строковых параметров. Так как по умолчанию в Visual Basic все пересылаемые переменные есть variable-праметры (ByRef - дословно - по ссылке), то неуказание такой директивы в нашем случае (никакой защиты мы не реализовали) приведет в лучшем случае к вездесущей недопустимой операции.

В процессе изучения Visual Basic я наткнулся на один весьма интересный момент. Такого понятия, как статическая линковка библиотеки, там попросту не существует! То есть все внешние вызовы носят только динамический характер. Это дает возможность приложению спокойно загрузиться и работать при отсутствии необходимой DLL ровненько до того момента, покуда в тексте программы не встретится вызов какой-либо из библиотечных функций. С другой стороны, явно запрограммировать, как это делается на Delphi с помощью LoadLibrary, динамическое подключение из Visual Basic попросту невозможно. Не получается сделать это из него и напрямую, через Windows API-функции для работы с DLL. То есть четкого стандартного разделения на динамику и статику в этом вопросе для Visual Basic не существует. Такой, с точки зрения нормального программиста, минус, на самом деле лишний раз демонстрирует характер, политику, если хотите, Visual Basic - как можно дальше абстрагироваться от программирования в смысле тупого кодирования и дать возможность продвинутому и, возможно, далекому от программирования пользователю реализовывать свои идеи. Чего-то я отвлекся.

Далее, для удобства дальнейшего использования импортированных процедур воспользуемся весьма распространенным приемом переобъявления:

Public Function Integer_To_Binary(ByVal n
As Long) As String
Integer_To_Binary = String(32, Chr(0))
IntToBin n, Integer_To_Binary
End Function
Public Function Binary_To_Integer(s As String) As Long
BinToInt s, Binary_To_Integer
End Function

Почему именно так? Во-первых, нам требовались функции, а не процедуры - мы их получили. Во-вторых, в функции Integer_To_Binary удовлетворена необходимость начальной разметки строки. Если передать импортированной IntToBin просто пустую текстовую переменную, произойдет ошибка, так как никакой памяти под ее содержимое Visual Basic-ом выделено еще не было. Для корректной работы IntToBin искусственно создается строка длиной в 32 нулевых символа, и лишь только затем передается библиотечной процедуре. Такой подход обеспечивает выделение необходимого количества байтов по пересылаемому адресу (переменной) для последующей работы IntToBin.

С точки зрения переваривания поступающих на вход процедурам библиотеки данных, они скорее напоминают утонченного гурмана или, что точнее, застарелого язвенника, нежели нормального человеко-желудка, способного питаться ржавыми гвоздями (сказал...). Другими словами, BinToInt рассчитывает лишь на строку нулей и единиц длиной ровненько в 32 символа, так же как для IntToBin требуется столько же места для записи последовательности цифр. Следующие функции можно отнести в разряд "косметических", но в то же время выполняющих роль "защиты от дурака".

Public Function FormatTo32(bs As String) As String
'дополнение двоичной строки нулями до 32-ух знаков слева
FormatTo32 = String(32 - Len(bs), Chr(48)) + Mid(bs, 1, Len(bs))
End Function
Public Function FormatZeroes(bs As String) As String
'удаление ведущих нулей двоичной строки слева
p& = InStr(1, bs, "1")
If p& = 0 Then
FormatZeroes = "0"
Else
FormatZeroes = Mid(bs, p&, Len(bs) - p& + 1)
End If
End FunctionВыглядеть прожка будет как на рисунке, и тогда следующий фрагмент кода разумеющему да будет прозрачен. Последний штрих:

Private Sub Text1_KeyPress(KeyAscii As Integer)
If KeyAscii = 13 Then
Text2.Text = Binary_To_Integer(FormatTo32(Text1.Text))
End If
End Sub
Private Sub Text2_KeyPress(KeyAscii As Integer)
If KeyAscii = 13 Then
On Error GoTo 1
n& = Val(Text2.Text)
Text1.Text = FormatZeroes(Integer_To_Binary(n))
Exit Sub
1: Text1.Text = "Переполнение..."
End If
End Sub

Заметили, насколько удобно использовать переопределенные через Integer_To_Binary и Binary_To_Integer неповоротливые процедуры IntToBin и BinToInt?

Если все было сделано по инструкции, перед нами должно оказаться даже весьма неламерское приложение, написанное и на Visual Basic в том числе. Кстати, совсем неслучайно я написал функции библиотеки практически полностью на ассемблере - во-первых, это наиболее простой способ реализации поставленной задачи, а во-вторых, пусть теперь кто-нибудь скажет, что с использованием Basic нельзя написать быстро работающее приложение. По этому поводу много кто чего говорит, но лично для себя я давно поделил людей (по отношению к Visual Basic) на три категории.

У первых и HalloWorld тормозит, что уж говорить об их остальных шедеврах. Вторые - те, кто собрались моделировать на нем ядерный взрыв и скорость обработки информации для которых важна настолько, что лучше сразу же, не теряя ни минуты, застрелиться с горя, чем сесть и убедиться, что на вопли "по поводу" потрачено куда больше времени, чем на выполнение любой из могущих быть ими написанных программ. Наконец, третьи (без лишней скромности к ним себя и отношу) - люди, которые ясно себе представляют, что можно на нем писать и как, а что не следует и когда.

Пожалуй, на этом можно остановиться. В этой статье я изложил лишь основные положения технологии DLL под Windows. Пространство для дальнейшего исследования данного вопроса еще осталось.

К примеру - экспорт данных a-la DLL. А чего только стоит проблема организации так называемых shared DLLs - библиотек, размещаемых в shared-памяти и, соответственно, общих для всех запущенных процессов. Вот где возможно полноценное использование глобальных переменных, действительная экономия памяти, организация интерфейса между приложениями. Ввиду довольно серьезных проблем с синхронизацией доступа и общей сложности работы с shared памятью в Windows привести более менее доходчивое до продвинутого пользователя описание этого раздела здесь мне возможным не представляется. Оставим заниматься этим системных программистов, хотя очень даже возможно, что меня снова вдохновят на подвиги ратные замечательные Cradle Of Filth и в следующий раз я посвящу n-ое количество килобайт текста как раз такого рода задачам.

Александр Муравский, БГУ ММФ "Компьютерная математика"


Компьютерная газета. Статья была опубликована в номере 14 за 2000 год в рубрике программирование :: разное

©1997-2024 Компьютерная газета