Utilisation de la bibliothèque FPC InternetPCools dans Delphi

En fait, l'article est un peu plus large - il décrit un moyen d'utiliser de manière transparente de nombreuses autres bibliothèques (et pas seulement du monde de Free Pascal ), et InternetTools a été choisi en raison de sa propriété remarquable - c'est le cas quand (étonnamment) est manquant Version Delphi avec les mêmes fonctionnalités et facilité d'utilisation.

Cette bibliothèque est conçue pour extraire des informations (analyse) de documents Web (XML et HTML), vous permettant d'utiliser des langages de requête de haut niveau tels que XPath et XQuery pour indiquer les données requises et, en option, offrant un accès direct à éléments d'un arbre construit sur le document.

Introduction aux InternetTools


D'autres éléments seront illustrés sur la base d'une tâche assez simple, ce qui implique d'obtenir les éléments des listes à puces et numérotées de cet article qui contiennent des liens, pour lesquels, si vous regardez la documentation , un si petit code suffit (il est construit sur la base de l'avant-dernier exemple avec de petites modifications sans principes) ):

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. 

Cependant, maintenant ce code compact et orienté objet ne peut être écrit qu'en Free Pascal, mais nous devons être en mesure d'utiliser tout ce que cette bibliothèque fournit dans une application Delphi, et de préférence dans un style similaire, avec les mêmes commodités; Il est également important de noter qu'InternetTools est sûr pour les threads (y accéder est autorisé à partir de plusieurs threads en même temps), donc notre option devrait le fournir.

Méthodes d'implémentation


Si vous abordez la tâche le plus loin possible, il y a plusieurs façons d'utiliser quelque chose écrit dans une autre langue - ils formeront 3 grands groupes:

  1. Placer la bibliothèque dans un processus séparé , dont le fichier exécutable est créé par les forces, dans ce cas, FPC . Cette méthode peut également être divisée en deux catégories pour une communication réseau possible:
  2. Encapsulation d'une bibliothèque dans une DLL (ci-après parfois dénommée «bibliothèque dynamique»), travaillant, par définition, dans le cadre d'un processus unique. Bien que les objets COM puissent être placés dans des DLL, l'article considérera une méthode plus simple et moins longue, qui offre, avec tout cela, le même confort lors de l'appel des fonctionnalités de la bibliothèque.
  3. Portage Comme dans les cas précédents, la pertinence de cette approche - réécrire du code dans une autre langue - est déterminée par l'équilibre entre ses avantages et ses inconvénients, mais dans la situation avec InternetTools, les inconvénients du portage sont beaucoup plus importants, à savoir: en raison de la quantité considérable de code de bibliothèque, vous devrez effectuer un travail très sérieux (même en tenant compte de la similitude des langages de programmation), et aussi périodiquement, en raison du développement du portable , la tâche de transférer les corrections et les nouvelles fonctionnalités vers Delphi apparaîtra.

Dll


De plus, afin de donner au lecteur la possibilité de ressentir la différence, il existe 2 options qui se distinguent par la commodité de leur utilisation.

Implémentation "classique"


Essayons de commencer à utiliser InternetTools dans un style procédural dicté par la nature même d'une bibliothèque dynamique qui ne peut exporter que des fonctions et des procédures; nous allons rendre la façon de communiquer avec la DLL similaire à WinAPI, lorsque le handle d'une certaine ressource est d'abord demandé, après quoi un travail utile est effectué, puis le handle résultant est détruit (fermé). Il n'est pas nécessaire de considérer cette option comme un modèle dans tout - elle est choisie uniquement pour la démonstration et la comparaison ultérieure avec la seconde - une sorte de parent pauvre.

La composition et la propriété des fichiers de la solution proposée ressembleront à ceci (les flèches indiquent les dépendances):

La composition des fichiers de l'implémentation "classique"


Module InternetTools.Types


Étant donné que les deux langages - Delphi et Free Pascal - sont très similaires dans ce cas, il est très raisonnable de sélectionner un tel module commun contenant les types utilisés dans la liste d'exportation DLL, afin de ne pas dupliquer leur définition dans l'application InternetToolsUsage . , qui inclut des prototypes de fonctionnalités d'une bibliothèque dynamique:

 unit InternetTools.Types; interface type TXQHandle = Integer; implementation end. 

Dans cette implémentation, un seul type timide est défini, mais à l'avenir, le module «mûrira» et son utilité deviendra indéniable.

Bibliothèque dynamique InternetTools


La composition des procédures et fonctions de la DLL est choisie pour être minimale, mais suffisante pour la mise en œuvre de la tâche ci-dessus:

 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. 

En raison de la nature démo de l'implémentation actuelle, le code complet n'est pas donné - beaucoup plus important est de savoir comment cette API la plus simple sera utilisée plus tard. Ici, vous n'avez tout simplement pas besoin d'oublier l'exigence de sécurité des threads, qui, bien que cela nécessitera des efforts, mais ce ne sera pas quelque chose de compliqué.

InternetToolsUsage Application


Grâce aux préparatifs précédents, il est devenu possible de réécrire l' exemple de liste dans 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. 

Si vous ne prenez pas en compte les prototypes de fonctions et procédures de la bibliothèque dynamique, vous ne pouvez pas dire que le code est catastrophiquement plus lourd par rapport à la version sur Free Pascal, mais que si nous compliquons un peu la tâche et essayons de filtrer certains éléments et d'afficher les adresses de liens contenues dans restant:

 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. 

Il est possible de le faire avec la DLL API actuelle, mais la verbosité du résultat est déjà très élevée, ce qui réduit non seulement considérablement la lisibilité du code, mais aussi (et non moins important) l'éloigne de ce qui précède:

 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. 

De toute évidence - dans des cas réels et plus complexes, le volume d'écriture ne fera que croître rapidement, et nous allons donc passer à une solution qui est exempte de tels problèmes.

Implémentation de l'interface


Le style procédural de travailler avec la bibliothèque, comme on vient de le montrer, est possible, mais présente des inconvénients importants. Étant donné que la DLL en tant que telle prend en charge l'utilisation d'interfaces (comme les types de données reçus et renvoyés), vous pouvez organiser le travail avec InternetTools de la même manière pratique que lorsque vous l'utilisez avec Free Pascal. Dans ce cas, il est souhaitable de modifier légèrement la composition des fichiers afin de répartir la déclaration et l'implémentation des interfaces dans des modules séparés:

La composition des fichiers d'implémentation de l'interface

Comme précédemment, nous examinerons chacun des fichiers en séquence.

Module InternetTools.Types


Déclare les interfaces à implémenter dans la 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. 

Les directives de compilation conditionnelle sont nécessaires en raison de l'utilisation du module inchangé dans les projets Delphi et FPC.

L'interface IXQValueEnumerator en principe facultative, cependant, afin de pouvoir utiliser des boucles de la forme " for ... in ... " comme exemple , vous ne pouvez pas vous en passer; la deuxième interface est la principale et est un wrapper analogique sur IXQValue d'InternetTools (il a été spécialement conçu du même nom pour faciliter la corrélation du futur code Delphi avec la documentation de la bibliothèque sur Free Pascal). Si nous considérons le module en termes de modèles de conception, les interfaces déclarées sont des adaptateurs , bien qu'avec une petite fonctionnalité - leur implémentation est située dans une bibliothèque dynamique.

La nécessité de définir le type d'appel safecall pour toutes les méthodes est bien décrite ici . L'obligation d'utiliser WideString au lieu de chaînes «natives» ne sera pas non plus étayée, car le sujet de l'échange de structures de données dynamiques avec des DLL dépasse le cadre de cet article.

Module InternetTools.Realization


La première en termes d'importance et de volume - c'est lui qui, comme le reflète le titre, contiendra l'implémentation des interfaces de la précédente: pour chacune d'elles, la seule classe TXQValue se voit TXQValue , dont les méthodes sont si simples que presque toutes consistent en une seule ligne de code (c'est assez attendu, car toutes les fonctionnalités nécessaires sont déjà contenues dans la bibliothèque - ici, il vous suffit d'y accéder):

 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. 

Cela vaut la peine de s'arrêter à la méthode SafeCallException - son blocage, dans l'ensemble, n'est pas vital ( TXQValue performances TXQValue n'en souffriront pas), cependant, le code donné ici vous permet de passer le texte d'exception du côté Delphi qui se produira dans les méthodes de safecall (détails, encore une fois, se trouve dans un article récent).

Cette solution, en plus de tout le reste, est IXQValue threads - à condition que IXQValue reçue, par exemple via OpenURL , ne soit pas transmise entre les threads. Cela est dû au fait que la mise en œuvre de l'interface ne redirige que les appels vers les InternetTools déjà thread-safe.

Bibliothèque dynamique InternetTools


En raison du travail effectué dans les modules ci-dessus, il suffit que la DLL exporte une seule fonction (comparer avec l' option où le style procédural a été utilisé):

 library InternetTools; uses InternetTools.Types, InternetTools.Realization; function GetXQValue: IXQValue; stdcall; begin Result := TXQValue.Create; end; exports GetXQValue; begin SetMultiByteConversionCodePage(CP_UTF8); end. 

L'appel à la procédure SetMultiByteConversionCodePage conçu pour fonctionner correctement avec les chaînes Unicode.

InternetToolsUsage Application


Si nous formalisons maintenant la solution Delphi de l' exemple initial sur la base des interfaces proposées, elle ne différera guère de celle de Free Pascal, ce qui signifie que la tâche définie au tout début de l'article peut être considérée comme terminée:

 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. 

Le module System.Win.ComObj n'est pas connecté accidentellement - sans lui, le texte de toutes les exceptions safecall deviendra une «exception dans la méthode safecall» sans visage, et avec lui la valeur d'origine générée dans la DLL.

Un exemple légèrement compliqué a également des différences minimes sur 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. 

Les fonctionnalités restantes de la bibliothèque


Si vous regardez toutes les capacités de l'interface IXQValue d'InternetTools, vous verrez que l' interface correspondante d' InternetTools.Types définit seulement 2 méthodes ( Map et ToString ) de l'ensemble riche; ajouter le reste que le lecteur juge nécessaire dans son cas particulier s'effectue exactement de la même manière et simplement: les méthodes nécessaires sont écrites dans InternetTools.Types , après quoi elles sont construites dans le module InternetTools.Realization avec du code (le plus souvent sur une seule ligne).

Si vous devez utiliser une fonctionnalité légèrement différente, par exemple, la gestion des cookies, la séquence des étapes est très similaire:

  1. Une nouvelle interface est InternetTools.Types dans InternetTools.Types :

     ... ICookies = interface ['{21D0CC9A-204D-44D2-AF00-98E9E04412CD}'] procedure Add(const URL, Name, Value: WideString); safecall; procedure Clear; safecall; end; ... 
  2. Il est ensuite implémenté dans le module 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. Après cela, une nouvelle fonction exportée est retournée à la DLL qui renvoie cette interface:

     ... function GetCookies: ICookies; stdcall; begin Result := TCookies.Create; end; exports ..., GetCookies; ... 

Libération de ressources


Bien que la bibliothèque InternetTools soit basée sur des interfaces qui contrôlent automatiquement la durée de vie, il existe une nuance non évidente qui semblerait entraîner des fuites de mémoire - si vous exécutez la prochaine application console (créée sur Delphi, rien ne changera dans le cas de FPC), puis à chaque fois que vous appuyez sur la touche entrée, la mémoire consommée par le processus augmente:

 ... 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. 

Il n'y a pas d'erreur avec l'utilisation des interfaces ici. Le problème est qu'InternetTools ne libère pas ses ressources internes allouées pendant l'analyse du document (dans la méthode OpenURL ) - cela doit être fait explicitement une fois le travail avec lui terminé; à ces fins, le module de bibliothèque xquery fournit la procédure freeThreadVars , dont l'appel à partir d'une application Delphi peut être logiquement assuré en développant la liste d'exportation DLL:

 ... procedure FreeResources; stdcall; begin freeThreadVars; end; exports ..., FreeResources; ... 

Après son utilisation, la perte de ressources s'arrêtera:

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

Il est important de comprendre ce qui suit - un appel à FreeResources conduit au fait que toutes les interfaces précédemment reçues FreeResources sens et toute tentative de les utiliser est inacceptable.

Source: https://habr.com/ru/post/fr415617/


All Articles