Шаг 306.
Создание внутрипроцессных серверов автоматизации. Создание и использование DLL. Обмен данными с DLL (окончание)

    На этом шаге мы закончим изучение данного вопроса.

    Можно использовать переменную типа PChar для получения указателя из DLL, но в этом случае в DLL должна быть объявлена глобальная переменная, предназначенная для хранения текстовой информации:

procedure ReceiveBuffer(var P: PChar); stdcall;
  external 'FirstLib.dll' name 'ReceiveBuffer';

procedure TForm1.Button1Click(Sender: TObject);
var
  S: String;
  P: PChar;
begin
  ReceiveBuffer(P);
  S := StrPas(P);
  Caption := S;
end;

    В DLL данная функция реализована следующим образом:

var
  {переменная не должна быть локальной!}
  Buffer:array [0..1000] of Char;

procedure ReceiveBuffer(var P: PChar); stdcall;  export;
var
  S: String;
  N: Integer;
begin
  FillMemory(@Buffer[0], SizeOf(Buffer), 0);
  if InputQuery('Задайте строку, которая будет отправлена в приложение',
     'Строка', S) then
     begin
       N := Length(S);
       if N > SizeOf(Buffer) - 1 then N := SizeOf(Buffer) - 1;
       if N > 0 then System.Move(S[1], Buffer[0], N);
     end;
  P := @Buffer[0];
end;
Текст этого приложения вместе с DLL можно взять здесь (414,0 Кб).

    Результат работы приложения изображен на рисунке 1.


Рис.1. Результат работы приложения

    Буфер нельзя определять как локальную переменную - после отработки функции ReceiveBuffer в DLL стек, куда помещаются локальные переменные, будет разрушен, и возвращаемый указатель будет ссылаться на недоступную область памяти.

    Аналогично, с помощью буфера можно передавать любые двоичные данные между приложением и DLL. Однако если размер двоичных данных варьируется в широких пределах, то могут возникнуть проблемы, связанные с размером буфера. Например, размер OLE-документов может варьироваться от нескольких десятков байтов до нескольких десятков мегабайтов. При использовании описанной выше технологии размер буфера должен быть равным максимально возможному размеру данных. Но если объявить буфер размером, скажем, 50 Мбайт, то большинство современных компьютеров будут создавать временное хранилище на диске. При этом передача даже небольших документов потребует заметного времени.

    Выход из данной ситуации заключается в резервировании памяти для хранения объекта в DLL и освобождении системных ресурсов в приложении, но без использования диспетчеров памяти приложения и DLL. Пример реализации этой идеи приведен ниже:

procedure ReceiveWinAPI(var HMem:Integer); stdcall;
  external 'FirstLib.dll' name 'ReceiveWinAPI';

procedure TForm1.Button1Click(Sender: TObject);
var
  H: Integer;
  S: String;
  P: PChar;
begin
  S := '';
  ReceiveWinAPI(H);
  if H > 0 then
  begin
    P := GlobalLock(H);
    S := StrPas(P);
    GlobalUnlock(H);
    GlobalFree(H);
  end;
  Caption := S;
end;

    Реализация в DLL функции ReceiveWinAPI:

procedure ReceiveWinAPI(var HMem:integer); stdcall; export;
var
  S:string;
  N:Integer;
  P:PChar;
begin
  HMem := 0;
  if InputQuery('Задайте строку, которая будет отправлена в приложение',
     'Строка', S) then
  begin
    N:=length(S);
    if N>0 then begin
      HMem := GlobalAlloc(GMEM_DDESHARE or GMEM_MOVEABLE
        or GMEM_ZEROINIT,N+1);
      if HMem>0 then begin
        P:=GlobalLock(HMem);
        if Assigned(P) then System.Move(S[1],P[0],N);
      end;
    end;
  end;
end;
Текст этого приложения вместе с DLL можно взять здесь (414,1 Кб).

    Результат работы приложения изображен на рисунке 2.


Рис.2. Результат работы приложения

    Здесь память резервируется в DLL, а освобождается в приложении. Для резервирования и освобождения памяти используются соответственно функции GlobalA11ос и Global Free Windows API. Памяти резервируется ровно столько, сколько необходимо для хранения объекта. Для приведенного выше примера с OLE-документами это означает, что в большинстве случаев обращения к виртуальной памяти на диске не потребуется, и, следовательно, обмен данными будет происходить быстро.

    Аналогично функциям Windows API можно использовать функции COM API для обмена данными между приложением и DLL. В COM API имеется пара функций CoTaskMemAlloc и CoTaskMemFree, которые определены в модуле ActiveX. Поскольку интерфейс COM API определен на уровне платформы, то вызовы этих функций можно успешно использовать для обмена данными между DLL и приложениями. Типичный пример кода для резервирования памяти выглядит следующим образом:

procedure ReceiveCOMAPI(var P:pointer); stdcall; export;
var
  S:string;
  N:integer;
begin
  P := nil;
  if InputQuery('Задайте строку, которая будет отправлена в приложение',
     'Строка', S) then
  begin
    N := length(S);
    if N>0 then begin
      P:=CoTaskMemAlloc(length(S)+1);
      if Assigned(P) then begin
        FillMemory(P,N+1,0);
        System.Move(S[1],P^,N);
      end;
    end;
  end;
end;

    Для резервирования памяти здесь используется вызов функции CoTaskMemAllос. При этом выделяется не менее чем length(S)+1 байтов памяти. Для проверки факта выделения полученный указатель сравнивается со значением nil. Выделенная память не инициализируется, поэтому обязательно следует использовать функцию FillMemory.

    В основном приложении напишем следующий код обработчика событий, связанного со щелчком на кнопке:

procedure ReceiveCOMAPI(var P: PChar); stdcall;
  external 'FirstLib.dll' name 'ReceiveCOMAPI';

procedure TForm1.Button1Click(Sender: TObject);
var
  S: String;
  P: PChar;
begin
  ReceiveCOMAPI(P);
  if Assigned(P) then
  begin
    S := StrPas(P);
    CoTaskMemFree(P);
  end;
  Caption := S;
end;
Текст этого приложения вместе с DLL можно взять здесь (414,1 Кб).

    Результат работы приложения изображен на рисунке 3.


Рис.3. Результат работы приложения

    В этом фрагменте кода для освобождения ресурсов, зарезервированных в другом модуле, используется процедура CoTaskMemFree.

    Из других средств обмена данными с DLL следует упомянуть модуль ShareMem. Именно сообщение о необходимости его использования можно прочитать в комментарии, который генерирует мастер Delphi при создании DLL. Применение этого модуля позволяет создать общий диспетчер памяти для приложения и DLL. Ссылка на модуль ShareMem должна быть объявлена первой в секции uses, так как он подменяет существующий диспетчер памяти. Если этого не сделать и часть памяти будет выделена существующим диспетчером памяти, то при попытке освободить ее в новом диспетчере получим исключение. Однако использовать этот модуль по следующим соображениям не рекомендуется:

    В СОМ передача строк осуществляется в переменных типа WideString - строка, где для хранения одного печатного символа используется два байта, а передача двоичных данных осуществляется в переменных типа OLEVariant, содержащих массив байтов. Помимо этих формальных признаков имеется и другое существенное отличие: ресурсы, хранимые в этих переменных и зарезервированные в одном модуле, могут быть корректно освобождены в другом модуле. Иногда использование переменных типа WideString или OLEVariant может быть нежелательным, так как в эти переменные осуществляется копирование данных. Если данные имеют большие размеры, то может возникнуть проблема с ресурсами. В этом случае следует реализовать доступ к данным через указатели, как это было описано в данных шагах.

    На следующем шаге мы рассмотрим вызов в DLL функций приложения.




Предыдущий шаг Содержание Следующий шаг