Использование FPC-библиотеки «InternetTools» в Delphi

На самом деле, статья несколько шире — она описывает способ, позволяющий прозрачно задействовать и многие другие библиотеки (причём не только из мира Free Pascal), а InternetTools выбрана из-за своего замечательного свойства — это тот случай, когда (как ни удивительно) отсутствует Delphi-вариант с такими же широкими возможностями и удобством использования.

Эта библиотека предназначена для извлечения информации (парсинга) из веб-документов (XML и HTML), позволяя использовать для указания нужных данных как языки запросов высокого уровня, такие как XPath и XQuery, так и, в качестве одного из вариантов, предоставляя прямой доступ к элементам дерева, построенного по документу.

Краткое знакомство с InternetTools


Дальнейший материал будет иллюстрироваться на основе довольно простой задачи, подразумевающей получение тех элементов маркированных и нумерованных списков данной статьи, что содержат ссылки, для чего, если обратиться к документации, достаточно такого небольшого кода (он построен на основе предпоследнего примера с внесением небольших, непринципиальных изменений):

uses
  xquery;

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
var
  ListValue: IXQValue;
begin
  for ListValue in xqvalue(ArticleURL).retrieve.map(ListXPath) do
    Writeln(ListValue.toString);
end.


Однако сейчас этот компактный и объектно-ориентированный код может быть написан лишь на Free Pascal, нам же требуется получить возможность задействовать всё, что предоставляет эта библиотека, в Delphi-приложении, причём желательно в аналогичном стиле, с теми же удобствами; также важно отметить, что InternetTools потокобезопасна (обращение к ней допустимо из многих потоков одновременно), поэтому и наш вариант должен обеспечивать это.

Способы реализации


Если подходить к задаче максимально издалека, то можно выделить несколько способов задействовать что-то, написанное на другом ЯП, — они составят 3 большие группы:

  1. Размещение библиотеки в отдельном процессе, исполняемый файл которого создаётся силами, в данном случае, FPC. Этот способ также может быть разбит на две категории по возможности сетевого общения:
  2. Инкапсуляция библиотеки в DLL (далее иногда «динамическая библиотека»), работающей, по определению, в рамках одного процесса. Хотя COM-объекты и могут быть размещены в DLL, статья рассмотрит более простой и менее трудоёмкий способ, дающий, при всём этом, тот же комфорт при вызове функционала библиотеки.
  3. Портирование. Как и в предыдущих случаях, целесообразность данного подхода — переписывания кода на другой язык — определяется балансом между его плюсами и минусами, но в ситуации с InternetTools недостатки портирования много больше, а именно: из-за немалого объёма кода библиотеки, потребуется проделать весьма серьёзную работу (даже с учётом схожести языков программирования), а также периодически, по причине развития портируемого, станет появляться задача переноса исправлений и новых возможностей в Delphi.


DLL


Далее, с целью предоставить читателю возможность ощутить разницу, приводятся 2 варианта, отличающиеся удобством своего применения.

«Классическая» реализация


Попробуем для начала использовать InternetTools в процедурном стиле, диктуемом самой природой динамической библиотеки, способной экспортировать лишь функции и процедуры; манеру общения с DLL сделаем похожей на WinAPI, когда сначала запрашивается дескриптор (handle) некоего ресурса, после чего выполняется полезная работа, а затем идёт уничтожение (закрытие) полученного дескриптора. Не нужно во всём рассматривать этот вариант как образец для подражания — он выбран лишь для демонстрации и последующего сравнения со вторым — своего рода бедный родственник.

Состав и принадлежность файлов предложенного решения будут выглядеть так (стрелками показаны зависимости):

Состав файлов «классической» реализации

Модуль InternetTools.Types


Т. к. в данном случае оба языка — Delphi и Free Pascal — являются очень похожими, то весьма разумно выделить такой общий модуль, содержащий типы, используемые в списке экспорта DLL, — это для того, чтобы затем не дублировать их определение в приложении InternetToolsUsage, включающем в себя прототипы функционала из динамической библиотеки:

unit InternetTools.Types;

interface

type
  TXQHandle = Integer;

implementation

end.


В данной реализации определён всего лишь один стыдливый тип, но в последующем модуль «повзрослеет» и его полезность станет несомненной.

Динамическая библиотека InternetTools


Состав процедур и функций DLL выбран минимальным, но достаточным для осуществления поставленной выше задачи:

library InternetTools;

uses
  InternetTools.Types;

function OpenDocument(const URL: WideString): TXQHandle; stdcall;
begin
  ...
end;

procedure CloseHandle(const Handle: TXQHandle); stdcall;
begin
  ...
end;

function Map(const Handle: TXQHandle; const XQuery: WideString): TXQHandle; stdcall;
begin
  ...
end;

function Count(const Handle: TXQHandle): Integer; stdcall;
begin
  ...
end;

function ValueByIndex(const Handle: TXQHandle; const Index: Integer): WideString; stdcall;
begin
  ...
end;

exports
  OpenDocument,
  CloseHandle,
  Map,
  Count,
  ValueByIndex;

begin

end.


Ввиду демонстрационного характера текущей реализации, полный код не приводится — много важнее то, как это простейшее API будет использоваться далее. Здесь только не нужно забывать о требовании потокобезопасности, которое пусть и потребует определённых усилий, но не явится чем-то сложным.

Приложение InternetToolsUsage


Благодаря предыдущим приготовлениям, стало возможно переписать пример со списками на Delphi:

program InternetToolsUsage;

...

uses
  InternetTools.Types;

const
  DLLName = 'InternetTools.dll';

function OpenDocument(const URL: WideString): TXQHandle; stdcall; external DLLName;
procedure CloseHandle(const Handle: TXQHandle); stdcall; external DLLName;
function Map(const Handle: TXQHandle; const XQuery: WideString): TXQHandle; stdcall; external DLLName;
function Count(const Handle: TXQHandle): Integer; stdcall; external DLLName;
function ValueByIndex(const Handle: TXQHandle; const Index: Integer): WideString; stdcall; external DLLName;

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
var
  RootHandle, ListHandle: TXQHandle;
  I: Integer;
begin
  RootHandle := OpenDocument(ArticleURL);
  try
    ListHandle := Map(RootHandle, ListXPath);
    try
      for I := 0 to Count(ListHandle) - 1 do
        Writeln( ValueByIndex(ListHandle, I) );
    finally
      CloseHandle(ListHandle);
    end;
  finally
    CloseHandle(RootHandle);
  end;

  ReadLn;
end.


Если не принимать во внимание прототипы функций и процедур из динамической библиотеки, то нельзя сказать, что код катастрофически утяжелился по сравнению с вариантом на Free Pascal, но что, если мы совсем немного усложним задачу и попробуем отфильтровать некоторые элементы и вывести адреса ссылок, содержащиеся в оставшихся:

uses
  xquery;

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
  HrefXPath = './a/@href';
var
  ListValue, HrefValue: IXQValue;
begin
  for ListValue in xqvalue(ArticleURL).retrieve.map(ListXPath) do
    if {Условие обработки элемента списка} then
      for HrefValue in ListValue.map(HrefXPath) do
        Writeln(HrefValue.toString);
end.


Сделать подобное с текущим API DLL возможно, но многословность получающегося уже весьма велика, что не только сильно снижает читаемость кода, но также (и это не менее важно) отдаляет его от вышеприведённого:

program InternetToolsUsage;

...

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
  HrefXPath = './a/@href';
var
  RootHandle, ListHandle, HrefHandle: TXQHandle;
  I, J: Integer;
begin
  RootHandle := OpenDocument(ArticleURL);
  try
    ListHandle := Map(RootHandle, ListXPath);
    try
      for I := 0 to Count(ListHandle) - 1 do
        if {Условие обработки элемента списка} then
        begin
          HrefHandle := Map(ListHandle, HrefXPath);
          try
            for J := 0 to Count(HrefHandle) - 1 do
              Writeln( ValueByIndex(HrefHandle, J) );
          finally
            CloseHandle(HrefHandle);
          end;
        end;
    finally
      CloseHandle(ListHandle);
    end;
  finally
    CloseHandle(RootHandle);
  end;

  ReadLn;
end.


Очевидно — в реальных, более комплексных случаях, объём написанного станет лишь стремительно расти, в связи с чем перейдём к решению, избавленному от подобных проблем.

Интерфейсная реализация


Процедурный стиль работы с библиотекой, как только что было показано, возможен, но имеет существенные недостатки. Благодаря тому, что DLL как таковая поддерживает использование интерфейсов (в качестве принимаемых и возвращаемых типов данных), можно организовать работу с InternetTools в той же удобной манере, что и при её применении с Free Pascal. Состав файлов при этом желательно немного поменять, чтобы распределить объявление и реализацию интерфейсов по отдельным модулям:

Состав файлов интерфейсной реализации


Как и до этого, последовательно рассмотрим каждый из файлов.

Модуль InternetTools.Types


Объявляет интерфейсы, подлежащие реализации в DLL:

unit InternetTools.Types;

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

interface

type
  IXQValue = interface;

  IXQValueEnumerator = interface
  ['{781B23DC-E8E8-4490-97EE-2332B3736466}']
    function MoveNext: Boolean; safecall;
    function GetCurrent: IXQValue; safecall;
    property Current: IXQValue read GetCurrent;
  end;

  IXQValue = interface
  ['{DCE33144-A75F-4C53-8D25-6D9BD78B91E4}']
    function GetEnumerator: IXQValueEnumerator; safecall;

    function OpenURL(const URL: WideString): IXQValue; safecall;
    function Map(const XQuery: WideString): IXQValue; safecall;
    function ToString: WideString; safecall;
  end;

implementation

end.


Директивы условной компиляции необходимы из-за использования модуля в неизменном виде как в Delphi-, так и в FPC-проекте.

Интерфейс IXQValueEnumerator в принципе необязателен, однако, чтобы иметь возможность использовать циклы вида »for ... in ...» как из примера, без него не обойтись; второй интерфейс основной и является обёрткой-аналогом над IXQValue из InternetTools (он специально сделан одноимённым, чтобы было проще соотносить будущий Delphi-код с библиотечной документацией на Free Pascal). Если рассматривать модуль в терминах шаблонов проектирования, то объявленные в нём интерфейсы представляют собой адаптеры, пусть и с небольшой особенностью — их реализация располагается в динамической библиотеке.

Необходимость задавать для всех методов тип вызова safecall хорошо описана здесь. Обязательность применения WideString вместо «родных» строк также не будет обосновываться, ибо тема по обмену динамическими структурами данных с DLL выходит за рамки статьи.

Модуль InternetTools.Realization


Первый и по важности, и по объёму — именно он, как отражено в названии, станет содержать реализацию интерфейсов из предыдущего: за оба из них ответственным назначен единственный класс TXQValue, методы которого настолько просты, что почти все состоят из одной строки кода (это вполне ожидаемо, ведь весь нужный функционал уже содержится в библиотеке — здесь всего-навсего требуется обратиться к нему):

unit InternetTools.Realization;

{$MODE Delphi}

interface

uses
  xquery,
  InternetTools.Types;

type
  IOriginalXQValue = xquery.IXQValue;

  TXQValue = class(TInterfacedObject, IXQValue, IXQValueEnumerator)
  private
    FOriginalXQValue: IOriginalXQValue;
    FEnumerator: TXQValueEnumerator;

    function MoveNext: Boolean; safecall;
    function GetCurrent: IXQValue; safecall;

    function GetEnumerator: IXQValueEnumerator; safecall;

    function OpenURL(const URL: WideString): IXQValue; safecall;
    function Map(const XQuery: WideString): IXQValue; safecall;
    function ToString: WideString; safecall; reintroduce;
  public
    constructor Create(const OriginalXQValue: IOriginalXQValue); overload;

    function SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult; override;
  end;

implementation

uses
  sysutils, comobj,
  w32internetaccess;

function TXQValue.MoveNext: Boolean;
begin
  Result := FEnumerator.MoveNext;
end;

function TXQValue.GetCurrent: IXQValue;
begin
  Result := TXQValue.Create(FEnumerator.Current);
end;

function TXQValue.GetEnumerator: IXQValueEnumerator;
begin
  FEnumerator := FOriginalXQValue.GetEnumerator;
  Result := Self;
end;

function TXQValue.OpenURL(const URL: WideString): IXQValue;
begin
  FOriginalXQValue := xqvalue(URL).retrieve;
  Result := Self;
end;

function TXQValue.Map(const XQuery: WideString): IXQValue;
begin
  Result := TXQValue.Create( FOriginalXQValue.map(XQuery) );
end;

function TXQValue.ToString: WideString;
begin
  Result := FOriginalXQValue.toJoinedString(LineEnding);
end;

constructor TXQValue.Create(const OriginalXQValue: IOriginalXQValue);
begin
  FOriginalXQValue := OriginalXQValue;
end;

function TXQValue.SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult;
begin
  Result := HandleSafeCallException(ExceptObject, ExceptAddr, GUID_NULL, ExceptObject.ClassName, '');
end;

end.


Стоит остановиться на методе SafeCallException — его перекрытие, по большому счёту, не является жизненно необходимым (работоспособность TXQValue ничуть без него не пострадает), однако приведённый здесь код позволяет передать на Delphi-сторону текст исключений, что будут возникать в safecall-методах (подробности, опять же, можно найти в уже приводившейся недавно статье).

Данное решение ко всему прочему является потокобезопасным — при условии, что IXQValue, полученный, например, через OpenURL, не передаётся между потоками. Это достигнуто за счёт того, что реализация интерфейса только перенаправляет вызовы уже потокобезопасной InternetTools.

Динамическая библиотека InternetTools


Из-за проделанной в модулях выше работы, DLL достаточно экспортировать единственную функцию (сравните с вариантом, где применялся процедурный стиль):

library InternetTools;

uses
  InternetTools.Types, InternetTools.Realization;

function GetXQValue: IXQValue; stdcall;
begin
  Result := TXQValue.Create;
end;

exports
  GetXQValue;

begin
  SetMultiByteConversionCodePage(CP_UTF8);
end.


Вызов процедуры SetMultiByteConversionCodePage предназначен для корректной работы с юникодовыми строками.

Приложение InternetToolsUsage


Если теперь оформить Delphi-решение изначального примера на основе предложенных интерфейсов, то оно почти не будет отличаться от такового на Free Pascal, а значит поставленная в самом начале статьи задача может считаться выполненной:

program InternetToolsUsage;

...

uses
  System.Win.ComObj,
  InternetTools.Types;

const
  DLLName = 'InternetTools.dll';

function GetXQValue: IXQValue; stdcall; external DLLName;

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
var
  ListValue: IXQValue;
begin
  for ListValue in GetXQValue.OpenURL(ArticleURL).Map(ListXPath) do
    Writeln(ListValue.ToString);

  ReadLn;
end.


Модуль System.Win.ComObj подключен не случайно — без него текст всех safecall-исключений станет представлять собой безликое «Exception in safecall method», а с ним — исходное значение, сгенерированное в DLL.

Чуть усложнённый пример аналогично имеет минимальные отличия на Delphi:

...

const
  ArticleURL = 'https://habr.com/post/415617';
  ListXPath = '//div[@class="post__body post__body_full"]//li[a]';
  HrefXPath = './a/@href';
var
  ListValue, HrefValue: IXQValue;
begin
  for ListValue in GetXQValue.OpenURL(ArticleURL).Map(ListXPath) do
    if {Условие обработки элемента списка} then
      for HrefValue in ListValue.Map(HrefXPath) do
        Writeln(HrefValue.ToString);

  ReadLn;
end.


Оставшийся функционал библиотеки


Если взглянуть на полные возможности интерфейса IXQValue из InternetTools, то станет видно, что соответствующий интерфейс из InternetTools.Types определяет лишь 2 метода (Map и ToString) из всего богатого набора; добавление оставшихся, что читатель сочтёт нужными в своём конкретном случае, выполняется абсолютно аналогично и просто: необходимые методы прописываются в InternetTools.Types, после чего в модуле InternetTools.Realization они наращиваются кодом (чаще всего в виде одной строки).

Если требуется задействовать несколько иную функциональность, для примера — управление куками, то последовательность шагов очень похожа:

  1. Объявляется новый интерфейс в InternetTools.Types:
    ...
    
    ICookies = interface
    ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}']
      procedure Add(const URL, Name, Value: WideString); safecall;
      procedure Clear; safecall;
    end;
    
    ...
    
  2. Затем он реализуется в модуле InternetTools.Realization:
    ...
    
    type
      TCookies = class(TInterfacedObject, ICookies)
      private
        procedure Add(const URL, Name, Value: WideString); safecall;
        procedure Clear; safecall;
      public
        function SafeCallException(ExceptObject: TObject; ExceptAddr: CodePointer): HResult; override;
      end;
    
    ...
    
    implementation
    
    uses
      ...,
      internetaccess;
    
    ...
    
    procedure TCookies.Add(const URL, Name, Value: WideString);
    begin
      defaultInternet.cookies.setCookie( decodeURL(URL).host, decodeURL(URL).path, Name, Value, [] );
    end;
    
    procedure TCookies.Clear;
    begin
      defaultInternet.cookies.clear;
    end;
    
    ...
    
  3. После чего в DLL поселяется новая экспортируемая функция, возвращающая данный интерфейс:
    ...
    
    function GetCookies: ICookies; stdcall;
    begin
      Result := TCookies.Create;
    end;
    
    exports
      ...,
      GetCookies;
    
    ...
    


Освобождение ресурсов


Хотя библиотека InternetTools и основана на интерфейсах, подразумевающих автоматическое управление временем жизни, но имеется один неочевидный нюанс, приводящий, казалось бы, к утечкам памяти — если запустить следующее консольное приложение (созданное на Delphi, но ничего не изменится и в случае с FPC), то при каждом нажатии клавиши ввода память, потребляемая процессом, станет расти:

...

const
  ArticleURL = 'https://habr.com/post/415617';
  TitleXPath = '//head/title';
var
  I: Integer;
begin
  for I := 1 to 100 do
  begin
    Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString );
    Readln;
  end;
end.


Каких-либо ошибок с применением интерфейсов здесь нет. Проблема заключается в том, что InternetTools не освобождает свои внутренние ресурсы, выделенные при анализе документа (в методе OpenURL), — это необходимо проделать явно, после того, как работа с ним закончена; для этих целей библиотечный модуль xquery предоставляет процедуру freeThreadVars, вызов которой из Delphi-приложения логично обеспечить за счёт расширения списка экспорта DLL:

...

procedure FreeResources; stdcall;
begin
  freeThreadVars;
end;

exports
  ...,
  FreeResources;

...


После её задействования потеря ресурсов прекратится:

for I := 1 to 100 do
begin
  Writeln( GetXQValue.OpenURL(ArticleURL).Map(TitleXPath).ToString );
  FreeResources;
  Readln;
end;


Важно понимать следующее — вызов FreeResources приводит к тому, что все ранее полученные интерфейсы становятся бессмысленными и любые попытки их использования недопустимы.

© Habrahabr.ru