Использование 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 приводит к тому, что все ранее полученные интерфейсы становятся бессмысленными и любые попытки их использования недопустимы.
    • +17
    • 3,1k
    • 7
    Поддержать автора
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      Всё что здесь описано так же подходит и для других ЯП… Как раз есть задумка писать ядро видеоплеера с ffmpeg на С++ и работать с ним через интерфейсы в Delphi.
        0
        к слову — может чем-то поможет:
        www.delphiffmpeg.com
          0
          В данный момент оно и используется, но есть много НО… использование SDL (для меня это минус), отсутствие аппаратного воспроизведения на любой видеокарте, очень сильно отстает товарищ от самого ffmpeg по версионности, оно и понятно портировать вагон заголовочных файлов с C++ на Delphi — то ещё удовольствие. Проще действительно писать ядро на C++ не портируя вообще ничего, а гуй на Delphi. Пока обходимся 4096x4096@60fps h264 на процессоре, но в планах 8х8k hevc main, паскали такое умеют аппаратно, правда вроде только 30fps. Очень надеюсь, что тьюринги смогут и 60fps для 8x8k. Перебросить текстуры из DX в GL — не проблема. Пока декодер софтварный, а стриминг текстур, нужные преобразования и отображение — OpenGL (GLSL) (возможно будет Vulkan, и поддержка Linux). Хочется отвязаться от SDL и его потоков и написать простое, своё, без кучи лишних вреймворков и библиотек, ну кроме ffmpeg, разумеется.
          Если интересно, это плеер для планетариев (одно- и много-проекторные системы)
        +1
        Если у читателя остались вопросы касательно тонкостей взаимодействия с DLL (т. е. не по InternetTools), то рекомендую серию статей, которая послужила основой для данной.
          0
          А можно поподробнее, в чём сложность полного портирования под Delphi?
            0
            1. InternetTools не является простой, состоящей из нескольких модулей, библиотекой: если брать в расчёт только inc- и pas-файлы, то их общее количество составит около 65-и, а размер примерно 5,5 МБ.
            2. Специфичность и низкоуровневость некоторых модулей: в качестве образца можно привести bbutils.pas, который способен подарить разработчику, до этого использовавшему строки только в прикладном ключе, новые неожиданные «удовольствия».
            3. Вопрос качества. Хотя библиотека и содержит автотесты (тоже, к слову, требующие портирования), их успешное прохождение не гарантирует отсутствия таких проблем, как, например, утечки памяти — это потребует отдельных тестовых сценариев.

            Косвенно трудоёмкость задачи можно оценить из того факта, что запрос на портирование существует уже 5 лет, однако до сих пор ни автор InternetTools, ни кто-то ещё подобное не сделали.
            0
            Последний раздел — «Освобождение ресурсов» — теперь, скорее всего, становится неактуальным — была сделана доработка по автоматическому освобождению в некоторых случаях.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое