Хабрахабр

Использование 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-проекте.

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

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

Модуль 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-методах (подробности, опять же, можно найти в уже приводившейся недавно статье).

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

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

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»