На лекции рассматриваются вопросы создания, а также различные способы использования динамически подключаемых библиотек - DLL.
Современное программирование стало достаточно сложным, уже написаны километры строк кода, и некоторые его части время от времени приходится дублировать в разных программах. Если бы каждый программист в каждой новой программе заново писал весь необходимый код, языки программирования никогда бы не получили такого стремительного развития. Однако мы в своих проектах можем использовать инструменты, созданные другими программистами - типы, процедуры, функции… Мы уже неоднократно подключали к нашим проектам различные модули и пользовались описанными в них средствами. И всё было бы хорошо, если бы не одно НО…
Дело в том, что при использовании модульного подхода, мы в момент компиляции проекта внедряем в него весь тот код, который был описан в модуле. И наша программа от такого внедрения "толстеет", добавляет иной раз по несколько мегабайт. Казалось бы, пустяки, современное аппаратное обеспечение достаточно хорошо развито, и мы можем себе это позволить. Но это все же тысячи строк чужого кода, причем далеко не весь этот код задействован в наших проектах! Теперь представьте, что вы пишете одну программу за другой, внедряете в нее одни и те же модули, в результате ваши проекты содержат мегабайты совершенно одинакового кода. И ладно бы только это, но представьте также, что вы одновременно загружаете и используете несколько программ (да здравствует многозадачность!). А эти программы тоже могут иметь в своем составе множество строк дублированного кода. Вот теперь у нас получается настоящее расточительство - мы впустую переводим не только память жесткого диска, где хранятся программы, но и оперативную память, куда мы их для использования загрузили!
Есть еще один недостаток модульного подхода - мы можем использовать чужой код, только если он написан на таком же языке программирования, каким пользуемся мы. Ну, в Lazarus мы можем использовать модули Delphi, а также код на Ассемблере. Но если код был написан на С++ или Visual Basic, к примеру? Короче говоря, требовался новый подход, позволяющий использовать чужой код, не внедряя его непосредственно в программу, и, кроме того, "языконезависимый". И таким подходом стала DLL.
Использование DLL дает нам еще одно очень важное преимущество. Представьте, что у вас десятки программ импортируют какой-то код. И вот, этот код нужно изменить, или дополнить, потому что вы перешли на другую ОС, на другие стандарты или просто потому, что начальство потребовало от вас расширить функциональность ваших программ. Если бы вы использовали модули, вам пришлось бы туго - нужно было бы переделывать не только этот модуль, но и перекомпилировать ваши программы. Но вы ведь их создавали не неделю и не месяц, правда? Что, если часть из них вы писали когда-то на Delphi 7, другую часть на Delphi XE3, а третью - уже на Lazarus? Работа была бы та еще. Но с использованием DLL эта задача сильно упрощается. Вам нужно переделать всего лишь эту самую DLL-библиотеку, а программы, использующие её, переделывать уже не нужно!
Каким же образом используются эти DLL? Фактически, DLL - это библиотека тех инструментов, которые могут быть использованы вами в различных проектах. Причем с учетом того, что вы, может быть, будете использовать различные языки программирования. Итак, первым делом, вы создаете саму библиотеку - файл *.dll. Создается он, как отдельное приложение, но после компиляции сам по себе работать все равно не сможет, поскольку не имеет собственной точки входа, как выполняемый exe-файл.
Далее, вы создаете приложение. Файл используемой динамической библиотеки вы помещаете в ту же папку, что и проект. В случае, если несколько ваших проектов будут использовать эту библиотеку, то её лучше поместить в какую-либо системную папку, например,
C:\Windows\system32
Тогда все эти программы смогут подгружать данную библиотеку, не указывая её адреса - Windows просматривает все свои системные папки и знает, где находится эта DLL. Функции и процедуры, описанные в DLL, наша программа будет воспринимать, как свои собственные. И когда во время выполнения программы происходит вызов таких функций и процедур, то будет подгружена соответствующая DLL.
Во время компиляции проекта код библиотеки не будет внедрен в программу. Это означает две вещи:
Когда пользователь загружает вашу программу, то также загружается и используемый dll-файл, на это тратится дополнительное время. Зато библиотека остается в оперативной памяти даже после завершения работы загрузившей её программы. И если затем будет загружена другая программа, использующая ту же DLL, она уже не будет тратить время на повторную загрузку этой библиотеки, а воспользуется уже загруженной в память версией. Более того, если будут одновременно работать несколько программ, использующих одну и ту же DLL, то они не будут загружать для себя по копии этой библиотеки, поскольку Windows гарантирует, что одновременно может быть загружена только одна копия динамической библиотеки. Так что первая из этих программ загрузит DLL в оперативную память, остальные будут использовать её же, не тратя лишнего времени на загрузку. Вот почему такие программы, как MS Word или MS Excel медленно загружаются в первый раз - они обращаются ко множеству библиотек, которые также подгружаются в память. Зато когда вы загружаете MS Word вторично, он грузится гораздо быстрее, так как нужные библиотеки уже в оперативной памяти! Почувствовали разницу между DLL и модулями?
Кстати, динамическим библиотекам принято давать расширение *.dll, однако это не всегда так. Вы можете дать такой библиотеке любое расширение, или даже вообще не указывать его! Например, драйверы устройств имеют расширение *.drv, но это такие же динамические библиотеки.
У DLL есть один недостаток: поскольку они языконезависимы, а также по некоторым другим причинам, в этих библиотеках нельзя использовать глобальные переменные, которые будут доступны в программе. Поэтому в DLL нельзя использовать объекты (свойства объектов - это глобальные переменные). Так что воспринимайте DLL, как набор процедур и функций, который удобно подгружать к различным программам.
В нашем курсе подразумевается, что мы используем операционную систему семейства Windows, однако механизм динамически подключаемых библиотек существует во всех современных операционных системах. В Windows такая библиотека представляет собой файл *.dll, в Linux и Unix - *.so (Shared Object Library), а в MacOS X - *.dylib (Dynamic Shared Library). И все эти библиотеки можно создавать с помощью Lazarus. Учитывая, что мы используем ОС Windows, мы будем рассматривать создание dll-файлов.
Загружаем Lazarus и выбираем команду "Файл -> Создать…". Откроется окно, в котором нам нужно будет выбрать "Библиотека":
(рис 26.1) Создание динамической библиотеки
В результате будет создан проект, в котором будет находиться следующий код:
library Project1;
{$mode objfpc}{$H+}
uses
Classes
{ you can add units after this };
begin
end.
Нажмите кнопку "Сохранить всё", и сохраните проект под именем MyFirstDLL в папку 26-01. Как видите, первая строка кода автоматически изменилась на
library MyFirstDLL;
а в указанной папке появилось три файла: MyFirstDLL.lpi, MyFirstDLL.lpr и MyFirstDLL.lps. Вы можете сразу же попробовать скомпилировать проект командой "Запуск -> Компилировать" или кнопками <Ctrl+F9> и в результате получите четвертый файл MyFirstDLL.dll. Это и есть динамическая библиотека, которая пока ещё ничего не умеет делать.
В разделе uses, после модуля Classes, через запятую мы можем добавить и другие, необходимые нам модули, однако не забывайте, что это будет увеличивать результирующий размер библиотеки. Поэтому нужно включать только те модули, без которых действительно, не обойтись. Обратите внимание - после последнего модуля точку с запятой ставить не нужно, она стоит после комментария, который сообщает, что вы можете добавлять модули после этого, то есть Classes. Комментарий можете оставить, а можете и удалить.
Весь основной код библиотеки должен находиться над завершающими begin…end. Для примера научим наш модуль делать несколько полезных вещей:
Поскольку нам придется иметь дело со строками, то следует иметь в виду вот что: в динамических библиотеках рекомендуют применять строковый тип PChar. Это связано с тем, что вашей DLL могут пользоваться программисты, создающие приложения на других платформах. В среде Windows тип PChar считается "родным", все WinAPI-функции используют этот тип. Внутри DLL-файла можно использовать любой строковый тип, но для передачи параметров и для получения результатов лучше использовать PChar.
Далее привожу полный код нашей библиотеки, после чего разберем некоторые моменты:
library MyFirstDLL;
{$mode objfpc}{$H+}
uses
Classes, DateUtils, SysUtils
{ you can add units after this };
const
R: array[1..13] of string[2] =
('I', 'IV', 'V', 'IX', 'X', 'XL', 'L', 'XC', 'C', 'CD', 'D', 'CM', 'M');
A: array[1..13] of Integer =
(1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000);
{шифрация/дешифрация}
function Code(s: PChar; Key: integer): PChar; stdcall;
var
i: integer;
ss: string;
begin
ss:= s;
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Result:= PChar(ss);
end;
{дней до очередного дня рождения}
function BeforeBirthday(Birthday:TDateTime): Integer; stdcall;
var
d,m,y: word; //день, месяц и год
mydate: TDateTime;
begin
mydate:= Birthday;
//разберем дату, изменим год на текущий и соберем обратно:
DecodeDate(mydate, y, m, d);
y:= YearOf(Date);
mydate:= EncodeDate(y, m, d);
//если ДР сегодня:
if mydate = Date then Result:= 0
//если будет:
else if mydate > Date then Result:= DaysBetween(Date, mydate)
//если уже был:
else begin
//установим дату ДР на следующий год и снова посчитаем:
y:= y + 1;
mydate:= EncodeDate(y, m, d);
Result:= DaysBetween(Date, mydate);
end; //else
end;
{Арабские в римские}
function ArToRom(N: integer): PChar; stdcall;
var
i: integer;
s: string;
begin
s := '';
i := 13;
while N > 0 do begin
while A[i] > N do Dec(i);
s := s + R[i];
Dec(N, A[i]);
end;
Result := PChar(s);
end;
{Римские в арабские}
function RomToAr(s: PChar): Integer; stdcall;
var
i, p: Integer;
begin
Result := 0;
i := 13;
p := 1;
while p <= Length(s) do begin
while Copy(s, p, Length(R[i])) <> R[i] do begin
Dec(i);
if i = 0 then Exit;
end; //while 2
Result := Result + A[i];
p := p + Length(R[i]);
end; //while 1
end;
exports
Code name 'Code',
BeforeBirthday name 'BeforeBirthday',
ArToRom name 'ArToRom',
RomToAr name 'RomToAr';
begin
end.
Разберем код. Прежде всего, мы включили в раздел uses два дополнительных модуля:
uses
Classes, DateUtils, SysUtils
{ you can add units after this };
Эти модули были необходимы для работы с датами - для получения текущей даты, для разборки даты на составляющие (год, месяц, день) и на обратную сборку, а также для определения количества дней от одной даты до другой.
Далее, у нас описаны две константы:
const
R: array[1..13] of string[2] =
('I', 'IV', 'V', 'IX', 'X', 'XL', 'L', 'XC', 'C', 'CD', 'D', 'CM', 'M');
A: array[1..13] of Integer =
(1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000);
Эти константы представляют собой массивы - первый строковый, на два символа, второй - массив целых чисел. Оба массива нужны для замены типа цифр. Например, римская III соответствует арабской 3, римская X - это арабская 10, и так далее. Тут следует иметь в виду, что отрицательных римских цифр не бывает, и что минимальная цифра - единица. Кроме того, тип integer имеет максимальное значение 2 147 483 647. Вряд ли вам потребуется переводить на римские большую цифру - римские обозначения не так популярны и используются, в основном, в литературе, для обозначения номера главы или раздела. Однако, помните, что мы включаем в библиотеку только инструменты конвертирования, а проверку допустимых значений должен делать программист в использующей нашу DLL программе.
Пойдем дальше. Первой у нас описана функция кодирования / декодирования строк:
{шифрация/дешифрация}
function Code(s: PChar; Key: integer): PChar; stdcall;
var
i: integer;
ss: string;
begin
ss:= s;
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Result:= PChar(ss);
end;
Здесь мы прежде всего видим, что функция принимает параметры - строку типа PChar и целое число - ключ, и возвращает также строку PChar, хотя внутри самой функции используется тип String. Еще мы видим, что после объявления имени функции указывается ключевое слово stdcall, означающее, что для вызова этой функции будет использовано стандартное соглашение.
Чтобы в программе можно было использовать инструменты из динамических библиотек сторонних разработчиков, были разработаны специальные соглашения по вызову процедур. Эти соглашения определяют различные правила вызова подпрограмм: как будут передаваться параметры - через стек, через регистры, через динамическую память; кто ответственный за очистку стека - вызывающая или вызываемая программа и т.д. Так, если используется стек, то чтение будет происходить справа налево. То есть, последние загруженные данные будут считаны первыми.
По умолчанию, используется соглашение register. Его имеет смысл использовать тогда, когда вы создаете на Lazarus и DLL, и использующую её программу. Такой вызов работает быстрее, однако он почти не применяется, так как обычно программисты предпочитают использовать более гибкие соглашения stdcall и cdecl.
Вызовы register и pascal передают параметры слева направо, то есть первый параметр слева вычисляется и передается в первую очередь, а последний параметр справа - вычисляется и передается последним. Вызовы cdecl и stdcall передают параметры наоборот, справа налево. В таблице ниже указаны способы вызова процедур:
| Команда вызова | Обработка параметров | Ответственный за очистку стека | Разрешена ли передача параметров через регистры |
|---|---|---|---|
Register |
Слева-направо | Подпрограмма | Да |
Pascal |
Слева-направо | Подпрограмма | Нет |
Cdecl |
Справа-налево | Вызывающая программа | Нет |
Stdcall |
Справа-налево | Подпрограмма | Нет |
Совсем уж влезать в дебри машинного языка, пожалуй, не стоит, запомните только несколько рекомендаций. Для программ на Windows чаще всего используют соглашение stdcall. А если вы создаете DLL, которую затем могут использовать Си-программисты, то указывайте соглашение cdecl.
Пойдем дальше. Команда
ss:= s;
не просто присваивает переменной ss переданную в параметре строку, она одновременно делает преобразование типов из PChar в String. Если вы не забыли, такие преобразования можно делать явно, например:
ss:= String(s);
s:= PChar(ss);
В нашей функции преобразование из PChar в String не указано, так как оно происходит по умолчанию, однако обратное преобразование все же было сделать необходимо:
Result:= PChar(ss);
Внутри подпрограммы устроен цикл
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Здесь, очередному символу строки ss присваивается результат работы побитовой операции XOR. Кстати, шифрование текста обычно делают именно с помощью XOR, наш пример только более простой. Что в данном случае делает XOR? Оператор XOR используется для инвертирования определённых битов числа (еще говорят, что XOR - операция исключающего ИЛИ). Для отдельных битов оператор XOR работает следующим образом (смотреть слева направо):
0 xor 0 = 0
0 xor 1 = 1
1 xor 0 = 1
1 xor 1 = 0
То есть, если два аргумента равны, XOR возвращает False (для отдельных битов - ноль). А если аргументы различаются, XOR возвращает True (или 1 для битов).
Каждый символ таблицы ASCII имеет собственное числовое обозначение. Так, английская "A" имеет номер 65, английская "B" - 66, и так далее, это всё мы обсуждали в лекции № 5. Компьютер работает только в двоичной системе, для него "A" - это не десятичная 65, а двоичная 0100 0001. Так вот, XOR обрабатывает не всё число целиком, а отдельно каждый его бит. В нашем примере слева от XOR указан очередной символ строки, а справа - число-ключ, от которого зависит изменение бита символа. Учитывая, что в таблице ASCII может быть максимум, 255 символов, слишком большой ключ указывать не стоит, во избежание ошибок. 10 вполне достаточно, хотя можете и поэкспериментировать (у меня после значения ключа 31 русские буквы при шифрации начинали искажаться, так что выбирайте значения от 1 до 30). Таким образом, мы каждый символ строки кодируем в совершенно другой символ.
В более сложных системах шифрования ключ может меняться от бита к биту.
Когда все символы строки обработаны, строка преобразуется в PChar, и результат возвращается обратно в программу.
Следующей у нас идет функция подсчета количества дней до следующего дня рождения. День рождения указывается в параметре, который имеет тип TDateTime. Чтобы подсчитать количество дней до очередного дня рождения, нам требуется получить новую дату - день и месяц дня рождения, а год мы должны указать текущий. Для этого мы вначале "разбираем" дату на составляющие:
DecodeDate(mydate, y, m, d);
Так мы получили отдельно, год, месяц и день. Затем мы изменили год на текущий:
y:= YearOf(Date);
После чего собрали дату обратно. Но это только полдела. Ведь может быть три варианта:
DaysBetween, которая возвращает разницу в днях между двумя указанными датами. Кстати, при желании вы можете использовать и другие подобные функции: YearsBetween - возвращает количество лет между двумя датами, MonthBetween - количество месяцев, WeeksBetween - количество недель, HoursBetween - количество часов, MinutesBetween - количество минут, SecondsBetween - количество секунд и MilliSecondsBetween - количество миллисекунд. Только имейте в виду, что если вам нужно получить разницу в часах, минутах, секундах или миллисекундах, то вместо Date, которая возвращает только текущую дату вам нужно будет использовать Now, которая возвращает текущие дату и время.Функции перевода римских цифр в арабские и обратно, для экономии места предлагаю рассмотреть самостоятельно - у нас ведь лекция про создание и вызов DLL, а не про синтаксис Паскаля. Если вы внимательно изучали предыдущие лекции, то сможете разобраться с синтаксисом. До этого мы не рассматривали только процедуру декремента Dec, которая уменьшает значения аргумента. По умолчанию, она уменьшает аргумент на единицу. Или можно указать аргумент и через запятую, значение, на которое нужно уменьшить аргумент:
Dec(i); //уменьшает i на единицу
Dec(N, A[i]); //уменьшает N на значение A[i]
С остальным синтаксисом вы должны быть уже знакомы.
В самом конце библиотеки у нас указан список функций, которые можно будет вызывать из внешних программ:
exports
Code name 'Code',
BeforeBirthday name 'BeforeBirthday',
ArToRom name 'ArToRom',
RomToAr name 'RomToAr';
В библиотеке может быть и больше процедур и функций, в списке exports нужно указывать только те, которые будут доступны извне. У этих же функций указывались и соглашения вызовов. В нашем случае, все имеющиеся функции должны быть доступны извне.
Теперь о способе описаний списка. Директива
Code name 'Code',
говорит о том, что наша функция Code должна вызываться из внешней программы по имени Code. Для чего это сделано? Допустим, во внешней программе уже есть функция Code, тогда мы получим конфликт имен. В этом случае мы могли бы вызывать подпрограмму под другим именем, например:
Code name 'NewCode',
Тогда во внешней программе мы запрашивали бы не функцию Code, а функцию NewCode.
Кроме того, в списке exports можно указывать передачу подпрограмм не по именам, а по индексам, например, так:
exports
Code, BeforeBirthday, ArToRom, RomToAr;
Тогда бы подпрограммам автоматически были бы присвоены индексы, от 1 до 4. Но индексы можно присвоить и принудительно, с помощью директивы index, например, так:
exports
Code index 1,
BeforeBirthday index 2,
ArToRom index 3,
RomToAr index 4;
Что нам дают индексы? В среде Windows подпрограммы по индексам вызываются чуть быстрее, однако запоминать, под каким индексом находится нужная подпрограмма сложнее. Кроме того, использование индексации имеет смысл только в Windows, так как другие ОС формируют индексы автоматически. В общем, рекомендуется обращаться к подпрограммам по именам, как мы и сделали в нашей библиотеке.
На этом работа с динамической библиотекой закончена. Поскольку кнопка Run на панели инструментов для DLL недоступна, нажмите <Ctrl+F9> или выберите команду "Запуск -> Компилировать". В результате получите готовую библиотеку - файл MyFirstDLL.dll.
Библиотеку DLL можно связать с приложением двумя способами:
Статическое связывание самое простое, оно подразумевает, что DLL будет загружена сразу, как только приложение начнет выполняться. Это самый простой способ использования DLL, обращение к процедурам и функциям динамической библиотеки такое же, как к обычным процедурам и функциям. Но есть и минусы использования статистического связывания.
Во-первых, при загрузке программы должны подгружаться и все используемые DLL, а это увеличивает время загрузки (при современной технике, правда, ненамного).
Во-вторых, чтобы пользователь мог работать с программой, он должен иметь все эти используемые DLL. Обычно конечно, так и есть, но бывают и исключения. Можно было бы раздать незарегистрированным пользователям усеченную версию программы, без некоторых ключевых DLL. Тогда они могли бы пользоваться программой, но не всеми её возможностями, а только основными. Зарегистрированным пользователям можно было бы дополнительно рассылать остальные DLL. При статическом связывании это недоступно.
В-третьих, при статическом связывании все загруженные DLL занимают память всё время, что работает программа, вне зависимости от того, использует ли она эти DLL в настоящее время, или нет.
Динамическое связывание подразумевает, что требуемая DLL будет подгружаться только тогда, когда требуется выполнить какую-то процедуру или функцию из неё. После чего DLL можно выгрузить, освободив память. Динамическое связывание также имеет плюсы и минусы. Плюсы в том, что программу можно использовать, не имея всех DLL. Загрузка программы будет осуществляться быстрее, поскольку DLL будут подгружаться по мере необходимости, а не вместе с программой.
Минусы же в том, что из-за необходимости подгружать и выгружать библиотеку, будет увеличиваться время обращения к её функциям и процедурам. Кроме того, само обращение к ним будет намного сложнее.
Разберем вначале более простой способ. Откройте Lazarus с новым проектом, имя формы измените на fMain, в Caption напишите "Статическое связывание DLL", установите свойства BorderStyle в bsDialog, а Position в poDesktopCenter. Нажмите кнопку "Сохранить всё" и сохраните проект под именем Proba в папку 26-02. Модуль формы назовите как обычно, Main.
Теперь скопируйте файл MyFirstDLL.dll из папки 26-01 в папку 26-02, чтобы наш проект мог пользоваться этой DLL.
Далее, перейдите в Редактор кода. Чтобы мы могли использовать функции и процедуры DLL, их нужно объявить так же, как в DLL. Объявления нужно делать сразу после ключевого слова implementation, и после директивы процессору {$R *.lfm}:
implementation
{$R *.lfm}
function Code(s: PChar; Key: integer): PChar; stdcall;
external 'MyFirstDLL.dll';
function BeforeBirthday(Birthday:TDateTime): Integer; stdcall;
external 'MyFirstDLL.dll';
function ArToRom(N: Integer): PChar; stdcall;
external 'MyFirstDLL.dll';
function RomToAr(s: PChar): Integer; stdcall;
external 'MyFirstDLL.dll';
Как видите, объявление требуемых нам функций такое же, каким было в DLL, но также после объявления была добавлена директива external:
external 'MyFirstDLL.dll';
которая означает, что эти функции программа должна искать в соответствующем файле динамической библиотеки, ведь программа может использовать несколько DLL. И это всё, что нужно для статического связывания. В дальнейшем мы сможем обращаться к этим функциям, как к любым другим.
Займемся компонентами. Установите на форму метку TLabel, в Caption которой напишите "Укажите день рождения:". Далее установите компонент TDateEdit с вкладки Misc Палитры компонентов, здесь пользователь будет вводить или выбирать требуемую для проверки дату рождения. Свойство Name компонента переименуйте в DE1. Правее установите простую кнопку TButton, в Caption которой напишите "До дня рождения", а Name переименуйте в bBeforeBirthday.
Ниже установим компоненты для проверки перевода формата цифр из арабских в римские, и наоборот. Вначале установим поясняющую метку TLabel, в Caption которой напишем "Укажите целое число от 1 до 2147483647:". Далее установим строку TEdit, которую переименуем в eNumbers, и очистим свойство Text. Ниже установим две простые кнопки TButton, которые переименуем в bArToRom и bRomToAr, а в Caption этих кнопок напишем соответственно, "Арабское в римское" и "Римское в арабское".
Наконец, займемся компонентами для шифрования текста. Сначала метку TLabel с текстом в Caption "Впишите текст:". Далее строку TEdit, которую переименуем в eCode, и у которой очистим свойство Text. Ниже установим простую кнопку TButton, переименуем её в bCode, и в свойстве Caption напишем "Шифровать/дешифровать". Измените положение и размеры компонентов так, чтобы ваша форма выглядела примерно так:
(рис 26.2) Форма приложения
Нам осталось написать код события OnClick для наших четырех кнопок, и опробовать в действии работу с DLL. Код OnClick для кнопки bBeforeBirthday будет такой:
procedure TfMain.bBeforeBirthdayClick(Sender: TObject);
begin
if DE1.Text = '' then exit;
ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней');
end;
Просто, не правда ли? Если пользователь не выбрал дня рождения, оставив строку DE1 пустой, мы выходим из процедуры, ничего не делая. Иначе мы выводим сообщение со сборной из трех частей строкой. Вызов функции из DLL:
IntToStr(BeforeBirthday(DE1.Date))
вернет нам в виде строки количество оставшихся до дня рождения дней.
Для кнопки bArToRom код события OnClick будет еще проще:
procedure TfMain.bArToRomClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
eNumbers.Text:= ArToRom(StrToInt(eNumbers.Text));
end;
Здесь мы делаем единственную проверку - не пуста ли строка eNumbers. Имейте в виду, что в реальном приложении вам еще потребовалась бы проверка на содержимое текста: число ли это, целое ли, входит ли оно в диапазон от 1 до 2 147 483 647? В нашей пробной программе для экономии места мы предполагаем, что пользователь не будет вводить некорректных данных.
Для кнопки bRomToAr код похожий:
procedure TfMain.bRomToArClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
eNumbers.Text:= IntToStr(RomToAr(PChar(eNumbers.Text)));
end;
В реальной программе вам также пришлось бы делать проверку на корректность ввода данных римских цифр. Здесь же мы будем считать, что пользователь ввел правильные римские цифры.
Ну и, наконец, для кнопки bCode код будет таким:
procedure TfMain.bCodeClick(Sender: TObject);
begin
if eCode.Text = '' then exit;
eCode.Text:= Code(PChar(eCode.Text), 10);
end;
Выше я советовал вам выбирать размер ключа в диапазоне от 1 до 30, в вызове функции Code из DLL я указал размер ключа 10. Вот, собственно, и все. Сохраните проект и запустите его. Если вы все сделали правильно, ваша программа будет работать, как надо:
(рис 26.3) Окно рабочей программы
Динамическое связывание происходит намного сложнее статического, причем динамическое связывание в Lazarus несколько отличается от такого же связывания в Delphi, поэтому примеры Delphi тут работать не будут.
Итак, начнем с того, что процедуру динамического связывания можно разбить на четыре этапа:
LoadLibrary и получение её дескриптора - числового идентификатора библиотеки в системе. Такой идентификатор представляет собой целое число. Если загрузка библиотеки была неудачной, то в дескриптор запишется ноль.GetProcAddress.Причем вы можете выполнять все действия по загрузке библиотеки в память и её выгрузке как для вызова каждой процедуры или функции отдельно, так и для вызова группы подпрограмм. Например, пользователь захотел выполнить некую операцию. Для этой операции требуется вызвать, скажем, десять подпрограмм (процедур и функций) из DLL. Будет глупо открывать и закрывать DLL отдельно для каждой подпрограммы, правильней будет открыть DLL, выполнить все эти десять подпрограмм, а затем закрыть DLL.
Но в нашем примере для наглядности мы будем загружать и выгружать DLL отдельно для каждой операции.
Для этого приложения нам потребуются точно такие же компоненты на форме, как и у предыдущего. Если вам не лень, то можно вернуться назад, и подготовить форму таким же образом, как и у проекта Proba. Однако можно сделать проще. Загрузите в Lazarus проект Proba из папки 26-02, для этого вам нужно загрузить файл Proba.lpi или Proba.lpr. Далее выведите на передний план Редактор формы. Щелкните правой кнопкой мыши по свободному месту формы и выберите команду "Выделить всё". При этом окажутся выделенными все компоненты формы. Затем выберите команду главного меню "Правка -> Копировать".
Теперь начнем новый проект. Выберите команду "Файл -> Создать -> Приложение", и у вас откроется новый проект с пустой формой. Растяните форму по ширине и высоте, чтобы на ней легко уместились все компоненты, позже размеры формы можно будет подкорректировать. Теперь выберите команду главного меню "Правка -> Вставить". При этом все нужные компоненты появятся на форме на том же самом месте. Кроме того, они сохранят свои имена и прочие настройки. Подкорректируйте размер формы. Теперь переименуйте форму в fMain, в Caption напишите "Динамическое связывание DLL", в BorderStyle выберите bsDialog, а в Position - poDesktopCenter. Сохраните проект в папку 26-03 под именем Proba2, модулю формы дайте имя Main. Не забудьте скопировать файл MyFirstDLL.dll и в эту папку.
Теперь нам потребуется некоторая подготовка перед тем, как мы начнем динамически связывать нашу DLL. Прежде всего, в раздел uses добавьте еще один модуль - Dynlibs. Именно в нём описаны необходимые инструменты для динамического подключения библиотек.
Далее, перед глобальным разделом var нам нужно описать типы наших подключаемых функций, а в самом разделе var добавить переменную-дескриптор библиотеки, а также по переменной на каждую функцию:
type
TCode = function(s: PChar; Key: integer): PChar; stdcall;
TBeforeBirthday = function(Birthday:TDateTime): Integer; stdcall;
TArToRom = function(N: integer): PChar; stdcall;
TRomToAr = function(s: PChar): Integer; stdcall;
var
fMain: TfMain;
MyH: THandle = 0; //для дескриптора библиотеки
Code: TCode;
BeforeBirthday: TBeforeBirthday;
ArToRom: TArToRom;
RomToAr: TRomToAr;
implementation
{$R *.lfm}
Обратите внимание, как мы описываем тип функций:
TCode = function(s: PChar; Key: integer): PChar; stdcall;
Тип мы назвали TCode, вы можете дать другое имя, но для типов и классов традиционно принято начинать имя с большой буквы "T". В этом типе мы указали, что создается функция с такими то параметрами (как в DLL), которая возвращает тип PChar, и будет использовать соглашение stdcall. При этом имени самой функции мы не указываем. Таким же образом мы создаем еще три типа.
Далее, в глобальном разделе var мы добавляем такую переменную:
MyH: THandle = 0; //для дескриптора библиотеки
Это - дескриптор. Когда мы загрузим DLL в память, в эту переменную попадет идентификатор нашей библиотеки. Ведь в памяти всё время загружено множество самых разных DLL, используемых системой, и Windows как-то нужно различать, к которой из них вы обращаетесь. По умолчанию сразу же выставляем нулевое значение. Если попытка считать DLL в память будет неудачной, например, эта DLL отсутствует, то дескриптор также будет нулевым.
Далее мы создаем по переменной для каждой вызываемой из DLL функции. Например, для функции Code мы создаем переменную
Code: TCode;
Тип TCode мы описали выше, здесь просто используем его же. В принципе, эти переменные необязательно делать глобальными, можно было описать их в разделе var той процедуры, откуда будем вызывать. Но в больших проектах одну и ту же DLL-функцию обычно вызывают из множества различных процедур, поэтому удобней описывать переменные, как глобальные.
Описав дескриптор и переменные для функций, мы сделали всю подготовительную работу.
Переходим ко второму этапу - динамической загрузке DLL, использовании нужной функции и выгрузке DLL из памяти.
Сгенерируйте событие OnClick для кнопки bBeforeBirthday. Код события отличается от прошлого проекта, он более сложный:
procedure TfMain.bBeforeBirthdayClick(Sender: TObject);
begin
//выходим, если пусто:
if DE1.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
BeforeBirthday:= TBeforeBirthday(GetProcAddress(MyH, 'BeforeBirthday'));
//если функция прочиталась, вычисляем, иначе выходим:
if @BeforeBirthday <> nil then ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Давайте подробно разбирать весь этот код. Для начала, как и в прошлом проекте, мы сделали проверку - есть ли что-то в строке DE1, и если нет, то выходим, так как в дальнейших действиях смысла нет. Если текст есть (пользователь вписал или выбрал дату рождения), то выполняется дальнейший код. Прежде всего, мы загружаем в память библиотеку, сразу же получая дескриптор на неё:
MyH:= LoadLibrary('MyFirstDLL.dll');
Функция LoadLibrary описана в модуле Dynlibs, который мы подключили в разделе uses, и пытается открыть указанную в параметре библиотеку. Если имя библиотечного файла указано без адреса, как в нашем случае, то файл библиотеки ищется в той же папке, откуда запущена программа. Если его там нет, то библиотека ищется в текущей папке (она может отличаться от папки с программой), затем в системных каталогах Windows и, наконец, в папках, указанных в системной переменной Path. Поэтому DLL лучше устанавливать в папку с программой, или в системную папку Windows, если эту DLL будут использовать несколько приложений. Такой папкой может быть, например,
C:\Windows\system32
Если файл MyFirstDLL.dll был найден, то LoadLibrary загрузит его в память, а дескриптор передаст в MyH. Если по какой то причине загрузить библиотеку не получилось, то LoadLibrary передаст в MyH значение 0. Поэтому дальше мы делаем проверку - успешно ли загрузилась библиотека? И если нет, то выводим соответствующее сообщение, и выходим:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
Далее, мы получаем адрес в памяти, где находится нужная нам процедура:
BeforeBirthday:= TBeforeBirthday(GetProcAddress(MyH, 'BeforeBirthday'));
Для этого мы указываем имя типа этой функции и используем функцию GetProcAddress. Эта функция работает так. В качестве параметров она получает дескриптор нужной библиотеки и название функции (процедуры), которую нам требуется оттуда вызвать. Затем она возвращает адрес этой функции в памяти, который попадает в нашу переменную BeforeBirthday. Кстати, необязательно давать этим переменным такие же имена, как и у функций, просто так удобней.
Если по какой то причине адрес функции получить не удалось, например, в памяти находится устаревшая версия DLL, где эта функция отсутствует, то GetProcAddress вернет значение nil, то есть, ничего. Именно поэтому мы делаем ещё одну проверку:
if @BeforeBirthday <> nil then ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
else ShowMessage('Нужная функция отсутствует в библиотеке');
Поскольку переменная BeforeBirthday является, на самом деле, указателем, то в ней находится не функция из DLL с аналогичным именем, а адрес этой функции в памяти. Чтобы посмотреть, указывает ли на что-то этот указатель, мы используем оператор получения адреса @, указанный перед именем указателя:
if @BeforeBirthday <> nil
Если по данному адресу находится значение, не равное nil, значит, требуемая функция доступна, и мы выводим сообщение, как в предыдущем проекте:
ShowMessage('До дня рождения осталось ' + IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
Иначе функция недоступна, о чем мы и сообщаем пользователю, не обращаясь при этом к функции:
else ShowMessage('Нужная функция отсутствует в библиотеке');
В принципе, в нашем небольшом проекте можно было бы обойтись и без всех этих многочисленных проверок, но я хотел показать, каким образом динамически подключаются DLL в реальных проектах. Это на первый взгляд весь этот код кажется сложным, а когда разберетесь и опробуйте его, он станет достаточно понятным. Под конец мы выгружаем DLL из памяти:
FreeLibrary(MyH);
После этого, обращаться к её функциям уже будет нельзя. Код обращения к остальным функциям мы так подробно разбирать не будем, поскольку он аналогичен рассмотренному выше коду. Код события OnClick кнопки bArToRom следующий:
procedure TfMain.bArToRomClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
ArToRom:= TArToRom(GetProcAddress(MyH, 'ArToRom'));
//если функция прочиталась, вычисляем, иначе выходим:
if @ArToRom <> nil then eNumbers.Text:= ArToRom(StrToInt(eNumbers.Text))
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Код события OnClick кнопки bRomToAr такой:
procedure TfMain.bRomToArClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
RomToAr:= TRomToAr(GetProcAddress(MyH, 'RomToAr'));
//если функция прочиталась, вычисляем, иначе выходим:
if @RomToAr <> nil then
eNumbers.Text:= IntToStr(RomToAr(PChar(eNumbers.Text)))
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
И, наконец, код OnClick кнопки bCode такой:
procedure TfMain.bCodeClick(Sender: TObject);
begin
if eCode.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
Code:= TCode(GetProcAddress(MyH, 'Code'));
//если функция прочиталась, вычисляем, иначе выходим:
if @Code <> nil then eCode.Text:= Code(PChar(eCode.Text), 10)
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Сохраните проект, запустите его на выполнение и убедитесь, что работа динамически связанной DLL ничем не отличается от статически связанной DLL. Замедления вызовов функций если и есть, то практически не заметны. Какой из этих методов выбирать, зависит от конкретной ситуации и предпочтений программиста. Я, к примеру, обычно использую статическую связку с DLL.
На лекции рассматриваются вопросы создания, а также различные способы использования динамически подключаемых библиотек - DLL.
Современное программирование стало достаточно сложным, уже написаны километры строк кода, и некоторые его части время от времени приходится дублировать в разных программах. Если бы каждый программист в каждой новой программе заново писал весь необходимый код, языки программирования никогда бы не получили такого стремительного развития. Однако мы в своих проектах можем использовать инструменты, созданные другими программистами - типы, процедуры, функции… Мы уже неоднократно подключали к нашим проектам различные модули и пользовались описанными в них средствами. И всё было бы хорошо, если бы не одно НО…
Дело в том, что при использовании модульного подхода, мы в момент компиляции проекта внедряем в него весь тот код, который был описан в модуле. И наша программа от такого внедрения "толстеет", добавляет иной раз по несколько мегабайт. Казалось бы, пустяки, современное аппаратное обеспечение достаточно хорошо развито, и мы можем себе это позволить. Но это все же тысячи строк чужого кода, причем далеко не весь этот код задействован в наших проектах! Теперь представьте, что вы пишете одну программу за другой, внедряете в нее одни и те же модули, в результате ваши проекты содержат мегабайты совершенно одинакового кода. И ладно бы только это, но представьте также, что вы одновременно загружаете и используете несколько программ (да здравствует многозадачность!). А эти программы тоже могут иметь в своем составе множество строк дублированного кода. Вот теперь у нас получается настоящее расточительство - мы впустую переводим не только память жесткого диска, где хранятся программы, но и оперативную память, куда мы их для использования загрузили!
Есть еще один недостаток модульного подхода - мы можем использовать чужой код, только если он написан на таком же языке программирования, каким пользуемся мы. Ну, в Lazarus мы можем использовать модули Delphi, а также код на Ассемблере. Но если код был написан на С++ или Visual Basic, к примеру? Короче говоря, требовался новый подход, позволяющий использовать чужой код, не внедряя его непосредственно в программу, и, кроме того, "языконезависимый". И таким подходом стала DLL.
Использование DLL дает нам еще одно очень важное преимущество. Представьте, что у вас десятки программ импортируют какой-то код. И вот, этот код нужно изменить, или дополнить, потому что вы перешли на другую ОС, на другие стандарты или просто потому, что начальство потребовало от вас расширить функциональность ваших программ. Если бы вы использовали модули, вам пришлось бы туго - нужно было бы переделывать не только этот модуль, но и перекомпилировать ваши программы. Но вы ведь их создавали не неделю и не месяц, правда? Что, если часть из них вы писали когда-то на Delphi 7, другую часть на Delphi XE3, а третью - уже на Lazarus? Работа была бы та еще. Но с использованием DLL эта задача сильно упрощается. Вам нужно переделать всего лишь эту самую DLL-библиотеку, а программы, использующие её, переделывать уже не нужно!
Каким же образом используются эти DLL? Фактически, DLL - это библиотека тех инструментов, которые могут быть использованы вами в различных проектах. Причем с учетом того, что вы, может быть, будете использовать различные языки программирования. Итак, первым делом, вы создаете саму библиотеку - файл *.dll. Создается он, как отдельное приложение, но после компиляции сам по себе работать все равно не сможет, поскольку не имеет собственной точки входа, как выполняемый exe-файл.
Далее, вы создаете приложение. Файл используемой динамической библиотеки вы помещаете в ту же папку, что и проект. В случае, если несколько ваших проектов будут использовать эту библиотеку, то её лучше поместить в какую-либо системную папку, например,
C:\Windows\system32
Тогда все эти программы смогут подгружать данную библиотеку, не указывая её адреса - Windows просматривает все свои системные папки и знает, где находится эта DLL. Функции и процедуры, описанные в DLL, наша программа будет воспринимать, как свои собственные. И когда во время выполнения программы происходит вызов таких функций и процедур, то будет подгружена соответствующая DLL.
Во время компиляции проекта код библиотеки не будет внедрен в программу. Это означает две вещи:
Когда пользователь загружает вашу программу, то также загружается и используемый dll-файл, на это тратится дополнительное время. Зато библиотека остается в оперативной памяти даже после завершения работы загрузившей её программы. И если затем будет загружена другая программа, использующая ту же DLL, она уже не будет тратить время на повторную загрузку этой библиотеки, а воспользуется уже загруженной в память версией. Более того, если будут одновременно работать несколько программ, использующих одну и ту же DLL, то они не будут загружать для себя по копии этой библиотеки, поскольку Windows гарантирует, что одновременно может быть загружена только одна копия динамической библиотеки. Так что первая из этих программ загрузит DLL в оперативную память, остальные будут использовать её же, не тратя лишнего времени на загрузку. Вот почему такие программы, как MS Word или MS Excel медленно загружаются в первый раз - они обращаются ко множеству библиотек, которые также подгружаются в память. Зато когда вы загружаете MS Word вторично, он грузится гораздо быстрее, так как нужные библиотеки уже в оперативной памяти! Почувствовали разницу между DLL и модулями?
Кстати, динамическим библиотекам принято давать расширение *.dll, однако это не всегда так. Вы можете дать такой библиотеке любое расширение, или даже вообще не указывать его! Например, драйверы устройств имеют расширение *.drv, но это такие же динамические библиотеки.
У DLL есть один недостаток: поскольку они языконезависимы, а также по некоторым другим причинам, в этих библиотеках нельзя использовать глобальные переменные, которые будут доступны в программе. Поэтому в DLL нельзя использовать объекты (свойства объектов - это глобальные переменные). Так что воспринимайте DLL, как набор процедур и функций, который удобно подгружать к различным программам.
В нашем курсе подразумевается, что мы используем операционную систему семейства Windows, однако механизм динамически подключаемых библиотек существует во всех современных операционных системах. В Windows такая библиотека представляет собой файл *.dll, в Linux и Unix - *.so (Shared Object Library), а в MacOS X - *.dylib (Dynamic Shared Library). И все эти библиотеки можно создавать с помощью Lazarus. Учитывая, что мы используем ОС Windows, мы будем рассматривать создание dll-файлов.
Загружаем Lazarus и выбираем команду "Файл -> Создать…". Откроется окно, в котором нам нужно будет выбрать "Библиотека":
(рис 26.1) Создание динамической библиотеки
В результате будет создан проект, в котором будет находиться следующий код:
library Project1;
{$mode objfpc}{$H+}
uses
Classes
{ you can add units after this };
begin
end.
Нажмите кнопку "Сохранить всё", и сохраните проект под именем MyFirstDLL в папку 26-01. Как видите, первая строка кода автоматически изменилась на
library MyFirstDLL;
а в указанной папке появилось три файла: MyFirstDLL.lpi, MyFirstDLL.lpr и MyFirstDLL.lps. Вы можете сразу же попробовать скомпилировать проект командой "Запуск -> Компилировать" или кнопками <Ctrl+F9> и в результате получите четвертый файл MyFirstDLL.dll. Это и есть динамическая библиотека, которая пока ещё ничего не умеет делать.
В разделе uses, после модуля Classes, через запятую мы можем добавить и другие, необходимые нам модули, однако не забывайте, что это будет увеличивать результирующий размер библиотеки. Поэтому нужно включать только те модули, без которых действительно, не обойтись. Обратите внимание - после последнего модуля точку с запятой ставить не нужно, она стоит после комментария, который сообщает, что вы можете добавлять модули после этого, то есть Classes. Комментарий можете оставить, а можете и удалить.
Весь основной код библиотеки должен находиться над завершающими begin…end. Для примера научим наш модуль делать несколько полезных вещей:
Поскольку нам придется иметь дело со строками, то следует иметь в виду вот что: в динамических библиотеках рекомендуют применять строковый тип PChar. Это связано с тем, что вашей DLL могут пользоваться программисты, создающие приложения на других платформах. В среде Windows тип PChar считается "родным", все WinAPI-функции используют этот тип. Внутри DLL-файла можно использовать любой строковый тип, но для передачи параметров и для получения результатов лучше использовать PChar.
Далее привожу полный код нашей библиотеки, после чего разберем некоторые моменты:
library MyFirstDLL;
{$mode objfpc}{$H+}
uses
Classes, DateUtils, SysUtils
{ you can add units after this };
const
R: array[1..13] of string[2] =
('I', 'IV', 'V', 'IX', 'X', 'XL', 'L', 'XC', 'C', 'CD', 'D', 'CM', 'M');
A: array[1..13] of Integer =
(1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000);
{шифрация/дешифрация}
function Code(s: PChar; Key: integer): PChar; stdcall;
var
i: integer;
ss: string;
begin
ss:= s;
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Result:= PChar(ss);
end;
{дней до очередного дня рождения}
function BeforeBirthday(Birthday:TDateTime): Integer; stdcall;
var
d,m,y: word; //день, месяц и год
mydate: TDateTime;
begin
mydate:= Birthday;
//разберем дату, изменим год на текущий и соберем обратно:
DecodeDate(mydate, y, m, d);
y:= YearOf(Date);
mydate:= EncodeDate(y, m, d);
//если ДР сегодня:
if mydate = Date then Result:= 0
//если будет:
else if mydate > Date then Result:= DaysBetween(Date, mydate)
//если уже был:
else begin
//установим дату ДР на следующий год и снова посчитаем:
y:= y + 1;
mydate:= EncodeDate(y, m, d);
Result:= DaysBetween(Date, mydate);
end; //else
end;
{Арабские в римские}
function ArToRom(N: integer): PChar; stdcall;
var
i: integer;
s: string;
begin
s := '';
i := 13;
while N > 0 do begin
while A[i] > N do Dec(i);
s := s + R[i];
Dec(N, A[i]);
end;
Result := PChar(s);
end;
{Римские в арабские}
function RomToAr(s: PChar): Integer; stdcall;
var
i, p: Integer;
begin
Result := 0;
i := 13;
p := 1;
while p <= Length(s) do begin
while Copy(s, p, Length(R[i])) <> R[i] do begin
Dec(i);
if i = 0 then Exit;
end; //while 2
Result := Result + A[i];
p := p + Length(R[i]);
end; //while 1
end;
exports
Code name 'Code',
BeforeBirthday name 'BeforeBirthday',
ArToRom name 'ArToRom',
RomToAr name 'RomToAr';
begin
end.
Разберем код. Прежде всего, мы включили в раздел uses два дополнительных модуля:
uses
Classes, DateUtils, SysUtils
{ you can add units after this };
Эти модули были необходимы для работы с датами - для получения текущей даты, для разборки даты на составляющие (год, месяц, день) и на обратную сборку, а также для определения количества дней от одной даты до другой.
Далее, у нас описаны две константы:
const
R: array[1..13] of string[2] =
('I', 'IV', 'V', 'IX', 'X', 'XL', 'L', 'XC', 'C', 'CD', 'D', 'CM', 'M');
A: array[1..13] of Integer =
(1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000);
Эти константы представляют собой массивы - первый строковый, на два символа, второй - массив целых чисел. Оба массива нужны для замены типа цифр. Например, римская III соответствует арабской 3, римская X - это арабская 10, и так далее. Тут следует иметь в виду, что отрицательных римских цифр не бывает, и что минимальная цифра - единица. Кроме того, тип integer имеет максимальное значение 2 147 483 647. Вряд ли вам потребуется переводить на римские большую цифру - римские обозначения не так популярны и используются, в основном, в литературе, для обозначения номера главы или раздела. Однако, помните, что мы включаем в библиотеку только инструменты конвертирования, а проверку допустимых значений должен делать программист в использующей нашу DLL программе.
Пойдем дальше. Первой у нас описана функция кодирования / декодирования строк:
{шифрация/дешифрация}
function Code(s: PChar; Key: integer): PChar; stdcall;
var
i: integer;
ss: string;
begin
ss:= s;
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Result:= PChar(ss);
end;
Здесь мы прежде всего видим, что функция принимает параметры - строку типа PChar и целое число - ключ, и возвращает также строку PChar, хотя внутри самой функции используется тип String. Еще мы видим, что после объявления имени функции указывается ключевое слово stdcall, означающее, что для вызова этой функции будет использовано стандартное соглашение.
Чтобы в программе можно было использовать инструменты из динамических библиотек сторонних разработчиков, были разработаны специальные соглашения по вызову процедур. Эти соглашения определяют различные правила вызова подпрограмм: как будут передаваться параметры - через стек, через регистры, через динамическую память; кто ответственный за очистку стека - вызывающая или вызываемая программа и т.д. Так, если используется стек, то чтение будет происходить справа налево. То есть, последние загруженные данные будут считаны первыми.
По умолчанию, используется соглашение register. Его имеет смысл использовать тогда, когда вы создаете на Lazarus и DLL, и использующую её программу. Такой вызов работает быстрее, однако он почти не применяется, так как обычно программисты предпочитают использовать более гибкие соглашения stdcall и cdecl.
Вызовы register и pascal передают параметры слева направо, то есть первый параметр слева вычисляется и передается в первую очередь, а последний параметр справа - вычисляется и передается последним. Вызовы cdecl и stdcall передают параметры наоборот, справа налево. В таблице ниже указаны способы вызова процедур:
| Команда вызова | Обработка параметров | Ответственный за очистку стека | Разрешена ли передача параметров через регистры |
|---|---|---|---|
Register |
Слева-направо | Подпрограмма | Да |
Pascal |
Слева-направо | Подпрограмма | Нет |
Cdecl |
Справа-налево | Вызывающая программа | Нет |
Stdcall |
Справа-налево | Подпрограмма | Нет |
Совсем уж влезать в дебри машинного языка, пожалуй, не стоит, запомните только несколько рекомендаций. Для программ на Windows чаще всего используют соглашение stdcall. А если вы создаете DLL, которую затем могут использовать Си-программисты, то указывайте соглашение cdecl.
Пойдем дальше. Команда
ss:= s;
не просто присваивает переменной ss переданную в параметре строку, она одновременно делает преобразование типов из PChar в String. Если вы не забыли, такие преобразования можно делать явно, например:
ss:= String(s);
s:= PChar(ss);
В нашей функции преобразование из PChar в String не указано, так как оно происходит по умолчанию, однако обратное преобразование все же было сделать необходимо:
Result:= PChar(ss);
Внутри подпрограммы устроен цикл
for i:= 1 to length(s) do
ss[i]:= char(Ord(ss[i]) xor Key);
Здесь, очередному символу строки ss присваивается результат работы побитовой операции XOR. Кстати, шифрование текста обычно делают именно с помощью XOR, наш пример только более простой. Что в данном случае делает XOR? Оператор XOR используется для инвертирования определённых битов числа (еще говорят, что XOR - операция исключающего ИЛИ). Для отдельных битов оператор XOR работает следующим образом (смотреть слева направо):
0 xor 0 = 0
0 xor 1 = 1
1 xor 0 = 1
1 xor 1 = 0
То есть, если два аргумента равны, XOR возвращает False (для отдельных битов - ноль). А если аргументы различаются, XOR возвращает True (или 1 для битов).
Каждый символ таблицы ASCII имеет собственное числовое обозначение. Так, английская "A" имеет номер 65, английская "B" - 66, и так далее, это всё мы обсуждали в лекции № 5. Компьютер работает только в двоичной системе, для него "A" - это не десятичная 65, а двоичная 0100 0001. Так вот, XOR обрабатывает не всё число целиком, а отдельно каждый его бит. В нашем примере слева от XOR указан очередной символ строки, а справа - число-ключ, от которого зависит изменение бита символа. Учитывая, что в таблице ASCII может быть максимум, 255 символов, слишком большой ключ указывать не стоит, во избежание ошибок. 10 вполне достаточно, хотя можете и поэкспериментировать (у меня после значения ключа 31 русские буквы при шифрации начинали искажаться, так что выбирайте значения от 1 до 30). Таким образом, мы каждый символ строки кодируем в совершенно другой символ.
В более сложных системах шифрования ключ может меняться от бита к биту.
Когда все символы строки обработаны, строка преобразуется в PChar, и результат возвращается обратно в программу.
Следующей у нас идет функция подсчета количества дней до следующего дня рождения. День рождения указывается в параметре, который имеет тип TDateTime. Чтобы подсчитать количество дней до очередного дня рождения, нам требуется получить новую дату - день и месяц дня рождения, а год мы должны указать текущий. Для этого мы вначале "разбираем" дату на составляющие:
DecodeDate(mydate, y, m, d);
Так мы получили отдельно, год, месяц и день. Затем мы изменили год на текущий:
y:= YearOf(Date);
После чего собрали дату обратно. Но это только полдела. Ведь может быть три варианта:
DaysBetween, которая возвращает разницу в днях между двумя указанными датами. Кстати, при желании вы можете использовать и другие подобные функции: YearsBetween - возвращает количество лет между двумя датами, MonthBetween - количество месяцев, WeeksBetween - количество недель, HoursBetween - количество часов, MinutesBetween - количество минут, SecondsBetween - количество секунд и MilliSecondsBetween - количество миллисекунд. Только имейте в виду, что если вам нужно получить разницу в часах, минутах, секундах или миллисекундах, то вместо Date, которая возвращает только текущую дату вам нужно будет использовать Now, которая возвращает текущие дату и время.Функции перевода римских цифр в арабские и обратно, для экономии места предлагаю рассмотреть самостоятельно - у нас ведь лекция про создание и вызов DLL, а не про синтаксис Паскаля. Если вы внимательно изучали предыдущие лекции, то сможете разобраться с синтаксисом. До этого мы не рассматривали только процедуру декремента Dec, которая уменьшает значения аргумента. По умолчанию, она уменьшает аргумент на единицу. Или можно указать аргумент и через запятую, значение, на которое нужно уменьшить аргумент:
Dec(i); //уменьшает i на единицу
Dec(N, A[i]); //уменьшает N на значение A[i]
С остальным синтаксисом вы должны быть уже знакомы.
В самом конце библиотеки у нас указан список функций, которые можно будет вызывать из внешних программ:
exports
Code name 'Code',
BeforeBirthday name 'BeforeBirthday',
ArToRom name 'ArToRom',
RomToAr name 'RomToAr';
В библиотеке может быть и больше процедур и функций, в списке exports нужно указывать только те, которые будут доступны извне. У этих же функций указывались и соглашения вызовов. В нашем случае, все имеющиеся функции должны быть доступны извне.
Теперь о способе описаний списка. Директива
Code name 'Code',
говорит о том, что наша функция Code должна вызываться из внешней программы по имени Code. Для чего это сделано? Допустим, во внешней программе уже есть функция Code, тогда мы получим конфликт имен. В этом случае мы могли бы вызывать подпрограмму под другим именем, например:
Code name 'NewCode',
Тогда во внешней программе мы запрашивали бы не функцию Code, а функцию NewCode.
Кроме того, в списке exports можно указывать передачу подпрограмм не по именам, а по индексам, например, так:
exports
Code, BeforeBirthday, ArToRom, RomToAr;
Тогда бы подпрограммам автоматически были бы присвоены индексы, от 1 до 4. Но индексы можно присвоить и принудительно, с помощью директивы index, например, так:
exports
Code index 1,
BeforeBirthday index 2,
ArToRom index 3,
RomToAr index 4;
Что нам дают индексы? В среде Windows подпрограммы по индексам вызываются чуть быстрее, однако запоминать, под каким индексом находится нужная подпрограмма сложнее. Кроме того, использование индексации имеет смысл только в Windows, так как другие ОС формируют индексы автоматически. В общем, рекомендуется обращаться к подпрограммам по именам, как мы и сделали в нашей библиотеке.
На этом работа с динамической библиотекой закончена. Поскольку кнопка Run на панели инструментов для DLL недоступна, нажмите <Ctrl+F9> или выберите команду "Запуск -> Компилировать". В результате получите готовую библиотеку - файл MyFirstDLL.dll.
Библиотеку DLL можно связать с приложением двумя способами:
Статическое связывание самое простое, оно подразумевает, что DLL будет загружена сразу, как только приложение начнет выполняться. Это самый простой способ использования DLL, обращение к процедурам и функциям динамической библиотеки такое же, как к обычным процедурам и функциям. Но есть и минусы использования статистического связывания.
Во-первых, при загрузке программы должны подгружаться и все используемые DLL, а это увеличивает время загрузки (при современной технике, правда, ненамного).
Во-вторых, чтобы пользователь мог работать с программой, он должен иметь все эти используемые DLL. Обычно конечно, так и есть, но бывают и исключения. Можно было бы раздать незарегистрированным пользователям усеченную версию программы, без некоторых ключевых DLL. Тогда они могли бы пользоваться программой, но не всеми её возможностями, а только основными. Зарегистрированным пользователям можно было бы дополнительно рассылать остальные DLL. При статическом связывании это недоступно.
В-третьих, при статическом связывании все загруженные DLL занимают память всё время, что работает программа, вне зависимости от того, использует ли она эти DLL в настоящее время, или нет.
Динамическое связывание подразумевает, что требуемая DLL будет подгружаться только тогда, когда требуется выполнить какую-то процедуру или функцию из неё. После чего DLL можно выгрузить, освободив память. Динамическое связывание также имеет плюсы и минусы. Плюсы в том, что программу можно использовать, не имея всех DLL. Загрузка программы будет осуществляться быстрее, поскольку DLL будут подгружаться по мере необходимости, а не вместе с программой.
Минусы же в том, что из-за необходимости подгружать и выгружать библиотеку, будет увеличиваться время обращения к её функциям и процедурам. Кроме того, само обращение к ним будет намного сложнее.
Разберем вначале более простой способ. Откройте Lazarus с новым проектом, имя формы измените на fMain, в Caption напишите "Статическое связывание DLL", установите свойства BorderStyle в bsDialog, а Position в poDesktopCenter. Нажмите кнопку "Сохранить всё" и сохраните проект под именем Proba в папку 26-02. Модуль формы назовите как обычно, Main.
Теперь скопируйте файл MyFirstDLL.dll из папки 26-01 в папку 26-02, чтобы наш проект мог пользоваться этой DLL.
Далее, перейдите в Редактор кода. Чтобы мы могли использовать функции и процедуры DLL, их нужно объявить так же, как в DLL. Объявления нужно делать сразу после ключевого слова implementation, и после директивы процессору {$R *.lfm}:
implementation
{$R *.lfm}
function Code(s: PChar; Key: integer): PChar; stdcall;
external 'MyFirstDLL.dll';
function BeforeBirthday(Birthday:TDateTime): Integer; stdcall;
external 'MyFirstDLL.dll';
function ArToRom(N: Integer): PChar; stdcall;
external 'MyFirstDLL.dll';
function RomToAr(s: PChar): Integer; stdcall;
external 'MyFirstDLL.dll';
Как видите, объявление требуемых нам функций такое же, каким было в DLL, но также после объявления была добавлена директива external:
external 'MyFirstDLL.dll';
которая означает, что эти функции программа должна искать в соответствующем файле динамической библиотеки, ведь программа может использовать несколько DLL. И это всё, что нужно для статического связывания. В дальнейшем мы сможем обращаться к этим функциям, как к любым другим.
Займемся компонентами. Установите на форму метку TLabel, в Caption которой напишите "Укажите день рождения:". Далее установите компонент TDateEdit с вкладки Misc Палитры компонентов, здесь пользователь будет вводить или выбирать требуемую для проверки дату рождения. Свойство Name компонента переименуйте в DE1. Правее установите простую кнопку TButton, в Caption которой напишите "До дня рождения", а Name переименуйте в bBeforeBirthday.
Ниже установим компоненты для проверки перевода формата цифр из арабских в римские, и наоборот. Вначале установим поясняющую метку TLabel, в Caption которой напишем "Укажите целое число от 1 до 2147483647:". Далее установим строку TEdit, которую переименуем в eNumbers, и очистим свойство Text. Ниже установим две простые кнопки TButton, которые переименуем в bArToRom и bRomToAr, а в Caption этих кнопок напишем соответственно, "Арабское в римское" и "Римское в арабское".
Наконец, займемся компонентами для шифрования текста. Сначала метку TLabel с текстом в Caption "Впишите текст:". Далее строку TEdit, которую переименуем в eCode, и у которой очистим свойство Text. Ниже установим простую кнопку TButton, переименуем её в bCode, и в свойстве Caption напишем "Шифровать/дешифровать". Измените положение и размеры компонентов так, чтобы ваша форма выглядела примерно так:
(рис 26.2) Форма приложения
Нам осталось написать код события OnClick для наших четырех кнопок, и опробовать в действии работу с DLL. Код OnClick для кнопки bBeforeBirthday будет такой:
procedure TfMain.bBeforeBirthdayClick(Sender: TObject);
begin
if DE1.Text = '' then exit;
ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней');
end;
Просто, не правда ли? Если пользователь не выбрал дня рождения, оставив строку DE1 пустой, мы выходим из процедуры, ничего не делая. Иначе мы выводим сообщение со сборной из трех частей строкой. Вызов функции из DLL:
IntToStr(BeforeBirthday(DE1.Date))
вернет нам в виде строки количество оставшихся до дня рождения дней.
Для кнопки bArToRom код события OnClick будет еще проще:
procedure TfMain.bArToRomClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
eNumbers.Text:= ArToRom(StrToInt(eNumbers.Text));
end;
Здесь мы делаем единственную проверку - не пуста ли строка eNumbers. Имейте в виду, что в реальном приложении вам еще потребовалась бы проверка на содержимое текста: число ли это, целое ли, входит ли оно в диапазон от 1 до 2 147 483 647? В нашей пробной программе для экономии места мы предполагаем, что пользователь не будет вводить некорректных данных.
Для кнопки bRomToAr код похожий:
procedure TfMain.bRomToArClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
eNumbers.Text:= IntToStr(RomToAr(PChar(eNumbers.Text)));
end;
В реальной программе вам также пришлось бы делать проверку на корректность ввода данных римских цифр. Здесь же мы будем считать, что пользователь ввел правильные римские цифры.
Ну и, наконец, для кнопки bCode код будет таким:
procedure TfMain.bCodeClick(Sender: TObject);
begin
if eCode.Text = '' then exit;
eCode.Text:= Code(PChar(eCode.Text), 10);
end;
Выше я советовал вам выбирать размер ключа в диапазоне от 1 до 30, в вызове функции Code из DLL я указал размер ключа 10. Вот, собственно, и все. Сохраните проект и запустите его. Если вы все сделали правильно, ваша программа будет работать, как надо:
(рис 26.3) Окно рабочей программы
Динамическое связывание происходит намного сложнее статического, причем динамическое связывание в Lazarus несколько отличается от такого же связывания в Delphi, поэтому примеры Delphi тут работать не будут.
Итак, начнем с того, что процедуру динамического связывания можно разбить на четыре этапа:
LoadLibrary и получение её дескриптора - числового идентификатора библиотеки в системе. Такой идентификатор представляет собой целое число. Если загрузка библиотеки была неудачной, то в дескриптор запишется ноль.GetProcAddress.Причем вы можете выполнять все действия по загрузке библиотеки в память и её выгрузке как для вызова каждой процедуры или функции отдельно, так и для вызова группы подпрограмм. Например, пользователь захотел выполнить некую операцию. Для этой операции требуется вызвать, скажем, десять подпрограмм (процедур и функций) из DLL. Будет глупо открывать и закрывать DLL отдельно для каждой подпрограммы, правильней будет открыть DLL, выполнить все эти десять подпрограмм, а затем закрыть DLL.
Но в нашем примере для наглядности мы будем загружать и выгружать DLL отдельно для каждой операции.
Для этого приложения нам потребуются точно такие же компоненты на форме, как и у предыдущего. Если вам не лень, то можно вернуться назад, и подготовить форму таким же образом, как и у проекта Proba. Однако можно сделать проще. Загрузите в Lazarus проект Proba из папки 26-02, для этого вам нужно загрузить файл Proba.lpi или Proba.lpr. Далее выведите на передний план Редактор формы. Щелкните правой кнопкой мыши по свободному месту формы и выберите команду "Выделить всё". При этом окажутся выделенными все компоненты формы. Затем выберите команду главного меню "Правка -> Копировать".
Теперь начнем новый проект. Выберите команду "Файл -> Создать -> Приложение", и у вас откроется новый проект с пустой формой. Растяните форму по ширине и высоте, чтобы на ней легко уместились все компоненты, позже размеры формы можно будет подкорректировать. Теперь выберите команду главного меню "Правка -> Вставить". При этом все нужные компоненты появятся на форме на том же самом месте. Кроме того, они сохранят свои имена и прочие настройки. Подкорректируйте размер формы. Теперь переименуйте форму в fMain, в Caption напишите "Динамическое связывание DLL", в BorderStyle выберите bsDialog, а в Position - poDesktopCenter. Сохраните проект в папку 26-03 под именем Proba2, модулю формы дайте имя Main. Не забудьте скопировать файл MyFirstDLL.dll и в эту папку.
Теперь нам потребуется некоторая подготовка перед тем, как мы начнем динамически связывать нашу DLL. Прежде всего, в раздел uses добавьте еще один модуль - Dynlibs. Именно в нём описаны необходимые инструменты для динамического подключения библиотек.
Далее, перед глобальным разделом var нам нужно описать типы наших подключаемых функций, а в самом разделе var добавить переменную-дескриптор библиотеки, а также по переменной на каждую функцию:
type
TCode = function(s: PChar; Key: integer): PChar; stdcall;
TBeforeBirthday = function(Birthday:TDateTime): Integer; stdcall;
TArToRom = function(N: integer): PChar; stdcall;
TRomToAr = function(s: PChar): Integer; stdcall;
var
fMain: TfMain;
MyH: THandle = 0; //для дескриптора библиотеки
Code: TCode;
BeforeBirthday: TBeforeBirthday;
ArToRom: TArToRom;
RomToAr: TRomToAr;
implementation
{$R *.lfm}
Обратите внимание, как мы описываем тип функций:
TCode = function(s: PChar; Key: integer): PChar; stdcall;
Тип мы назвали TCode, вы можете дать другое имя, но для типов и классов традиционно принято начинать имя с большой буквы "T". В этом типе мы указали, что создается функция с такими то параметрами (как в DLL), которая возвращает тип PChar, и будет использовать соглашение stdcall. При этом имени самой функции мы не указываем. Таким же образом мы создаем еще три типа.
Далее, в глобальном разделе var мы добавляем такую переменную:
MyH: THandle = 0; //для дескриптора библиотеки
Это - дескриптор. Когда мы загрузим DLL в память, в эту переменную попадет идентификатор нашей библиотеки. Ведь в памяти всё время загружено множество самых разных DLL, используемых системой, и Windows как-то нужно различать, к которой из них вы обращаетесь. По умолчанию сразу же выставляем нулевое значение. Если попытка считать DLL в память будет неудачной, например, эта DLL отсутствует, то дескриптор также будет нулевым.
Далее мы создаем по переменной для каждой вызываемой из DLL функции. Например, для функции Code мы создаем переменную
Code: TCode;
Тип TCode мы описали выше, здесь просто используем его же. В принципе, эти переменные необязательно делать глобальными, можно было описать их в разделе var той процедуры, откуда будем вызывать. Но в больших проектах одну и ту же DLL-функцию обычно вызывают из множества различных процедур, поэтому удобней описывать переменные, как глобальные.
Описав дескриптор и переменные для функций, мы сделали всю подготовительную работу.
Переходим ко второму этапу - динамической загрузке DLL, использовании нужной функции и выгрузке DLL из памяти.
Сгенерируйте событие OnClick для кнопки bBeforeBirthday. Код события отличается от прошлого проекта, он более сложный:
procedure TfMain.bBeforeBirthdayClick(Sender: TObject);
begin
//выходим, если пусто:
if DE1.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
BeforeBirthday:= TBeforeBirthday(GetProcAddress(MyH, 'BeforeBirthday'));
//если функция прочиталась, вычисляем, иначе выходим:
if @BeforeBirthday <> nil then ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Давайте подробно разбирать весь этот код. Для начала, как и в прошлом проекте, мы сделали проверку - есть ли что-то в строке DE1, и если нет, то выходим, так как в дальнейших действиях смысла нет. Если текст есть (пользователь вписал или выбрал дату рождения), то выполняется дальнейший код. Прежде всего, мы загружаем в память библиотеку, сразу же получая дескриптор на неё:
MyH:= LoadLibrary('MyFirstDLL.dll');
Функция LoadLibrary описана в модуле Dynlibs, который мы подключили в разделе uses, и пытается открыть указанную в параметре библиотеку. Если имя библиотечного файла указано без адреса, как в нашем случае, то файл библиотеки ищется в той же папке, откуда запущена программа. Если его там нет, то библиотека ищется в текущей папке (она может отличаться от папки с программой), затем в системных каталогах Windows и, наконец, в папках, указанных в системной переменной Path. Поэтому DLL лучше устанавливать в папку с программой, или в системную папку Windows, если эту DLL будут использовать несколько приложений. Такой папкой может быть, например,
C:\Windows\system32
Если файл MyFirstDLL.dll был найден, то LoadLibrary загрузит его в память, а дескриптор передаст в MyH. Если по какой то причине загрузить библиотеку не получилось, то LoadLibrary передаст в MyH значение 0. Поэтому дальше мы делаем проверку - успешно ли загрузилась библиотека? И если нет, то выводим соответствующее сообщение, и выходим:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
Далее, мы получаем адрес в памяти, где находится нужная нам процедура:
BeforeBirthday:= TBeforeBirthday(GetProcAddress(MyH, 'BeforeBirthday'));
Для этого мы указываем имя типа этой функции и используем функцию GetProcAddress. Эта функция работает так. В качестве параметров она получает дескриптор нужной библиотеки и название функции (процедуры), которую нам требуется оттуда вызвать. Затем она возвращает адрес этой функции в памяти, который попадает в нашу переменную BeforeBirthday. Кстати, необязательно давать этим переменным такие же имена, как и у функций, просто так удобней.
Если по какой то причине адрес функции получить не удалось, например, в памяти находится устаревшая версия DLL, где эта функция отсутствует, то GetProcAddress вернет значение nil, то есть, ничего. Именно поэтому мы делаем ещё одну проверку:
if @BeforeBirthday <> nil then ShowMessage('До дня рождения осталось ' +
IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
else ShowMessage('Нужная функция отсутствует в библиотеке');
Поскольку переменная BeforeBirthday является, на самом деле, указателем, то в ней находится не функция из DLL с аналогичным именем, а адрес этой функции в памяти. Чтобы посмотреть, указывает ли на что-то этот указатель, мы используем оператор получения адреса @, указанный перед именем указателя:
if @BeforeBirthday <> nil
Если по данному адресу находится значение, не равное nil, значит, требуемая функция доступна, и мы выводим сообщение, как в предыдущем проекте:
ShowMessage('До дня рождения осталось ' + IntToStr(BeforeBirthday(DE1.Date)) + ' дней')
Иначе функция недоступна, о чем мы и сообщаем пользователю, не обращаясь при этом к функции:
else ShowMessage('Нужная функция отсутствует в библиотеке');
В принципе, в нашем небольшом проекте можно было бы обойтись и без всех этих многочисленных проверок, но я хотел показать, каким образом динамически подключаются DLL в реальных проектах. Это на первый взгляд весь этот код кажется сложным, а когда разберетесь и опробуйте его, он станет достаточно понятным. Под конец мы выгружаем DLL из памяти:
FreeLibrary(MyH);
После этого, обращаться к её функциям уже будет нельзя. Код обращения к остальным функциям мы так подробно разбирать не будем, поскольку он аналогичен рассмотренному выше коду. Код события OnClick кнопки bArToRom следующий:
procedure TfMain.bArToRomClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
ArToRom:= TArToRom(GetProcAddress(MyH, 'ArToRom'));
//если функция прочиталась, вычисляем, иначе выходим:
if @ArToRom <> nil then eNumbers.Text:= ArToRom(StrToInt(eNumbers.Text))
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Код события OnClick кнопки bRomToAr такой:
procedure TfMain.bRomToArClick(Sender: TObject);
begin
if eNumbers.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
RomToAr:= TRomToAr(GetProcAddress(MyH, 'RomToAr'));
//если функция прочиталась, вычисляем, иначе выходим:
if @RomToAr <> nil then
eNumbers.Text:= IntToStr(RomToAr(PChar(eNumbers.Text)))
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
И, наконец, код OnClick кнопки bCode такой:
procedure TfMain.bCodeClick(Sender: TObject);
begin
if eCode.Text = '' then exit;
//открываем библиотеку и получаем ее дескриптор:
MyH:= LoadLibrary('MyFirstDLL.dll');
//выходим, если ошибка:
if MyH = 0 then begin
ShowMessage('Ошибка открытия библиотеки MyFirstDLL.');
exit;
end;
//получаем адрес нужной функции:
Code:= TCode(GetProcAddress(MyH, 'Code'));
//если функция прочиталась, вычисляем, иначе выходим:
if @Code <> nil then eCode.Text:= Code(PChar(eCode.Text), 10)
else ShowMessage('Нужная функция отсутствует в библиотеке');
//выгружаем библиотеку из памяти:
FreeLibrary(MyH);
end;
Сохраните проект, запустите его на выполнение и убедитесь, что работа динамически связанной DLL ничем не отличается от статически связанной DLL. Замедления вызовов функций если и есть, то практически не заметны. Какой из этих методов выбирать, зависит от конкретной ситуации и предпочтений программиста. Я, к примеру, обычно использую статическую связку с DLL.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.