Использование 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 большие группы:
- Размещение библиотеки в отдельном процессе, исполняемый файл которого создаётся силами, в данном случае, FPC. Этот способ также может быть разбит на две категории по возможности сетевого общения:
- Инкапсуляция библиотеки в DLL (далее иногда «динамическая библиотека»), работающей, по определению, в рамках одного процесса. Хотя COM-объекты и могут быть размещены в DLL, статья рассмотрит более простой и менее трудоёмкий способ, дающий, при всём этом, тот же комфорт при вызове функционала библиотеки.
- Портирование. Как и в предыдущих случаях, целесообразность данного подхода — переписывания кода на другой язык — определяется балансом между его плюсами и минусами, но в ситуации с 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
они наращиваются кодом (чаще всего в виде одной строки).
Если требуется задействовать несколько иную функциональность, для примера — управление куками, то последовательность шагов очень похожа:
- Объявляется новый интерфейс в
InternetTools.Types
:... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ...
- Затем он реализуется в модуле
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; ...
- После чего в 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
приводит к тому, что все ранее полученные интерфейсы становятся бессмысленными и любые попытки их использования недопустимы.