Учебный центр "ANIT Texno Inform"
Опубликован: 25.06.2014 | Доступ: свободный | Студентов: 2595 / 850 | Длительность: 24:39:00
Специальности: Программист
Лекция 23:

DLL

Динамическое связывание DLL

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

Итак, начнем с того, что процедуру динамического связывания можно разбить на четыре этапа:

  1. Загрузка динамической библиотеки функцией LoadLibrary и получение её дескриптора - числового идентификатора библиотеки в системе. Такой идентификатор представляет собой целое число. Если загрузка библиотеки была неудачной, то в дескриптор запишется ноль.
  2. Получение адреса нужной функции (или функций) с помощью GetProcAddress.
  3. Непосредственно работа с нужной функцией из DLL.
  4. Выгрузка DLL из памяти.

Причем вы можете выполнять все действия по загрузке библиотеки в память и её выгрузке как для вызова каждой процедуры или функции отдельно, так и для вызова группы подпрограмм. Например, пользователь захотел выполнить некую операцию. Для этой операции требуется вызвать, скажем, десять подпрограмм (процедур и функций) из 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.

Инга Готфрид
Инга Готфрид
Александр Скрябнев
Александр Скрябнев

Через WMI, или используя утилиту wmic? А может есть еще какие более простые пути...