RPG IV : Programmer avec des « handles »

Si vous êtes familiarisés avec les programmes de services stateful – c’est-à-dire utilisant des variables globales pour mémoriser des informations entre deux appels de procédures – vous percevrez rapidement l’intérêt de l’utilisation de ces handles qui concernent justement la gestion de ces données de persistance.

Ce billet est aussi une occasion – pour ceux qui ne les utilisent pas encore – de manipuler quelques outils tels que les pointeurs, les buffers dynamiques, les variables « based() » et autres joyeusetés.

Plutôt que de théoriser sur le sujet, je vous propose l’étude d’un cas simple et concret mettant en oeuvre une petite solution d’écriture de lignes de texte dans l’IFS. Il s’agit d’un service rudimentaire, ne permettant d’alimenter qu’un seul fichier à la fois. Il exporte cinq procédures permettant de :

  • Créer le fichier – ifstxt_creFichier()
  • Y ajouter des lignes – ifstxt_ajtLigne()
  • Le clôturer – ifstxt_ferFichier()
  • Récupérer le texte des éventuelles erreurs – ifstxt_infErreur()
  • Récupérer les informations statistiques qui nous intéressent – ifstxt_stt()

Au niveau client, son utilisation prend la forme suivante :

  

     h dftactgrp(*no) actgrp('NDP400')
     h bnddir('NDP400')

      /copy QRPGLEINC,IFSTXT1


     d msgErreur...
     d                 s            256a   varying

     d msgErreur52...
     d                 s             52a

      /free

       if ifstxt_creFichier('/tmp/test01.txt') = *on;
         ifstxt_ajtLigne('Une première ligne');
         ifstxt_ajtLigne('Une deuxième ligne');
         ifstxt_ajtLigne('Une troisième ligne');
         ifstxt_ferFichier();
       else;
         // Affiche l'erreur avec DSPLY (52 caractères max).
         msgErreur = ifstxt_infErreur();
         if %len(msgErreur) > 52;
           msgErreur52 = %subst(msgErreur: 1: 52);
         else;
           msgErreur52 = %subst(msgErreur: 1: %len(msgErreur));
         endif;
         dsply msgErreur52;
       endif;

       *inlr = *on;

      /end-free

Voici le source – expurgé des déclarations de constantes et prototypes propres aux traitements dans l’IFS – de l’unique module de notre service :

     h bnddir('QC2LE')
     h optimize(*none)
     h nomain

      /copy QRPGLEINC,IFSTXT1


      * - Constantes ------------------------------------------------------------
    
      // ... des constantes...

      *-- Prototypes externes ---------------------------------------------------
     
      // ... des prototypes...

      *-- Variables globales ----------------------------------------------------

      * Handle  du fichier IFS
     d g_hldIFS...
     d                 s             10i 0 inz(-1)

      * Nombre de ligne ajoutée
     d g_nbrLigne...
     d                 s             10i 0 inz

      * Nombre de caractere
     d g_nbrCarac...
     d                 s             10i 0 inz

      * Texte de la dernière erreur
     d g_txtErreur...
     d                 s             32a   varying



      *---------------------------------------------------------------------------------------------
      *
      * Créée le fichier
      *
      *---------------------------------------------------------------------------------------------
     p ifstxt_creFichier...
     p                 b                   export
     d                 pi              n
     d   fichier_...
     d                             1024a   const
     d
     d flags...
     d                 s             10i 0
     d mode...
     d                 s             10i 0
      /free

       if g_hldIFS >= 0;
         ifstxt_ferFichier();
       endif;

       clear g_nbrLigne;
       clear g_nbrCarac;
       clear g_txtErreur;

       // Création du fichier pour écriture en UTF-8
       flags = O_CREAT + O_WRONLY + O_CCSID + O_TEXTDATA + O_TEXT_CREAT;
       mode  = S_IRUSR + S_IWUSR + S_IRGRP + S_IROTH;
       g_hldIFS = IFSopen(%trimr(fichier_): flags: mode: 1208: 0);
       if (g_hldIFS < 0);
          g_p_errno__ = errno__();
          g_txtErreur = %str(strerror(g_errNbr));
          return *off;
       endif;
       return *on;
      /end-free
      p                 e



      *---------------------------------------------------------------------------------------------
      *
      * Ferme le fichier créé avec ifstxt_creFichier()
      *
      *---------------------------------------------------------------------------------------------
     p ifstxt_ferFichier...
     p                 b                   export
     d                 pi              n
      /free
        
       clear g_txtErreur;
       if g_hldIFS >= 0;
         IFSclose(g_hldIFS);
       endif;

       return *on;

      /end-free
     p                 e



      *---------------------------------------------------------------------------------------------
      *
      * Ajoute une ligne dans le fichier précédemment créé avec ifstxt_creFichier()
      *
      *---------------------------------------------------------------------------------------------
     p ifstxt_ajtLigne...
     p                 b                   export
     d                 pi
     d   txt_...
     d                            16384a   const varying
     d CRLF...
     d                 s              2a   inz(x'0D25') static
      /free

       clear g_txtErreur;

       // Contrôle qu'un fichier est bien ouvert.
       if g_hldIFS < 0;
         g_txtErreur = 'Aucun fichier n''est ouvert.';
         return;
       endif;

       g_nbrLigne += 1;

       if g_nbrLigne > 1;
         IFSWrite(g_hldIFS: %addr(CRLF): 2);
         g_nbrCarac += 2;
       endif;

       if %len(txt_) = 0;
         return;
       endif;

       IFSWrite(g_hldIFS: txt_: %len(txt_));
       g_nbrCarac += %len(txt_);

      /end-free
     p                 e 



      *---------------------------------------------------------------------------------------------
      *
      * Retourne le nombre de ligne ou de caratères ajoutées au fichier
      *
      *---------------------------------------------------------------------------------------------
     p ifstxt_stt...
     p                 b                   export
     d                 pi            10i 0
     d   info_...
     d                               10i 0 const
      /free

       if info_ = 0;
         return g_nbrLigne;
       else;
         return g_nbrCarac;
       endif;

      /end-free
     p                 e



      *---------------------------------------------------------------------------------------------
      *
      * Retourne le texte de la dernière erreur
      *
      *---------------------------------------------------------------------------------------------
     p ifstxt_infErreur...
     p                 b                   export
     d                 pi           256a   varying
      /free

       return g_txtErreur;

      /end-free
     p                 e
 

Ce qui nous intéresse ici, c’est le mécanisme permettant au service de conserver le contexte entre deux appels de procédure.

Dans notre cas, il utilise quatre variables globales :

  • Le handle (et oui…) du fichier IFS
  • Le nombre de lignes ajoutées
  • Le nombre de caractères ajoutés
  • Un texte d’erreur, si erreur il y a.

Nous souhaitons maintenant faire évoluer cette solution afin qu’elle permette l’alimentation simultanée de fichiers texte.

Vers une solution multi-instances

Avant tout, quelle que soit la méthode adoptée, il faudra bien évidement permettre aux programmes clients de désigner la ressource sur laquelle ils souhaitent opérer. Dans le cas de notre solution, cette ressource désigne le contexte d’alimentation du fichier texte. Attention : elle ne désigne pas « le fichier texte » en tant que tel, mais bien le contexte dans lequel il est produit car, une fois le fichier fermé, rien n’interdit d’appeler des fonctions de diagnostics renvoyant le temps pour produire le document, la taille du fichier, le nombre d’éléments ajoutés etc. Tant que la ressource n’est pas libérée le contexte est utilisable.

Le handle représente cette ressource. Du point de vue du programme client, c’est une variable faisant le lien entre deux appels de fonctions. Côté service, c’est un chemin d’accès à un contexte.

Ainsi, le déroulement d’un programme utilisant un tel service sera quasi invariablement :

  • Récupération d’un nouvel handle
  • Utilisation de ce handle avec toutes les fonctions concernées
  • Libération du handle

Evolutions côté client

Avant de nous pencher sur la transformation du service, je vous propose de voir comment il est perçu et utilisé par le client, en commençant par prendre connaissance des nouveaux prototypes :

      * Source QRPGLEINC,IFSTXT2   
    
      * Modèle de handle
     d m_ifstxt_handle...
     d                 s               *   based(null)


      *---------------------------------------------------------------------------------------------
      *
      * Retourne un nouvel handle. Celui-ci devra être libéré avec ifstxt_lbr()
      *
      * Retour :
      * - Un handle.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_nve...
     d                 pr                  like(m_ifstxt_handle)



      *---------------------------------------------------------------------------------------------
      *
      * Libère le handle passé en paramètre.
      *
      * Paramètre :
      *
      * - handle
      *   Le handle à libérer.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_lbr...
     d                 pr
     d   handle...
     d                                     like(m_ifstxt_handle) value



      *---------------------------------------------------------------------------------------------
      *
      * Créé un nouveau fichier qui devient le fichier "en cours" pour le handle donnée.
      *
      * Paramètres :
      *
      * - handle
      *   Le handle à utiliser
      *
      * - fichier
      *   Le chemin d'accès au fichier à crééer
      *
      * Retour :
      * - *on/*off. En cas d'erreur, utiliser ifstxt_infErreur() pour un texte clair.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_creFichier...
     d                 pr              n
     d   handle...
     d                                     like(m_ifstxt_handle) value
     d   fichier...
     d                             1024a   const
     d



      *---------------------------------------------------------------------------------------------
      *
      * Ferme le fichier ouvert avec ifstxt_creFichier()
      *
      * Paramètres :
      *
      * - handle
      *   Le handle à évaluer
      *
      * Retour :
      * - Le texte de l'erreur.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_ferFichier...
     d                 pr              n
     d   handle...
     d                                     like(m_ifstxt_handle) value



      *---------------------------------------------------------------------------------------------
      *
      * Ajoute une ligne au fichier.
      *
      * Paramètres :
      *
      * - handle
      *   Le handle à utiliser
      *
      * - txt
      *   Le texte à ajouter.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_ajtLigne...
     d                 pr
     d   handle...
     d                                     like(m_ifstxt_handle) value
     d   txt...
     d                            16384a   const varying



      *---------------------------------------------------------------------------------------------
      *
      * Permet de récupérer des informations sur l'alimentation du fichier.
      *
      * Paramètres :
      *
      * - handle
      *   Le handle à évaluer
      *
      * - info :
      *   0 ou 1, information à retourner.
      *   Respectivement : nombre de lignes ou nombre de caractères ajoutés
      *
      * Retour :
      * - Un entier.
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_stt...
     d                 pr            10i 0
     d   handle...
     d                                     like(m_ifstxt_handle) value
     d   info...
     d                               10i 0 const



      *---------------------------------------------------------------------------------------------
      *
      * Retourne le texte de la dernière erreur
      *
      *---------------------------------------------------------------------------------------------
     d ifstxt_infErreur...
     d                 pr           256a   varying
     d   handle...
     d                                     like(m_ifstxt_handle) value

Pour plus de lisibilité, le handle est désigné par une variable modèle, ici m_ifstxt_handle. Nous voyons aussi apparaître deux nouvelles procédures : ifstxt_nve() et ifstxt_lbr() pour créer et libérer des handles. Toutes les procédures sont mises à jour pour le recevoir.

Nous pouvons maintenant créer un programme client. Celui-ci crée simultanément deux fichiers et y ajoute quelques lignes.

     h dftactgrp(*no) actgrp('NDP400')
     h bnddir('NDP400')

      /copy QRPGLEINC,IFSTXT2


     d fichier1...
     d                 s                   like(m_ifstxt_handle)

     d fichier2...
     d                 s                   like(m_ifstxt_handle)

      /free

       // Création d'un premier handle
       fichier1 = ifstxt_nve();

       // Création d'un second handle
       fichier2 = ifstxt_nve();

       // Si aucune erreur, alimente les deux fichiers
       if (ifstxt_creFichier(fichier1: '/tmp/test02-A.txt') = *on
           and ifstxt_creFichier(fichier2: '/tmp/test02-B.txt') = *on);

         ifstxt_ajtLigne(fichier2: 'Une première ligne');
         ifstxt_ajtLigne(fichier1: 'Une première ligne');
         ifstxt_ajtLigne(fichier2: 'Une deuxième ligne');
         ifstxt_ajtLigne(fichier2: 'Une troisième ligne');
         ifstxt_ajtLigne(fichier1: 'Une deuxième ligne');
         ifstxt_ajtLigne(fichier1: 'Une troisième ligne');

         // Clôture les deux fichiers
         ifstxt_ferFichier(fichier1);
         ifstxt_ferFichier(fichier2);
       endif;

       // Libère les deux handles
       ifstxt_lbr(fichier2);
       ifstxt_lbr(fichier1);

       *inlr = *on;

      /end-free

Vous remarquerez que de son point de vue, le type de la variable représentant le handle – ci-dessous fichier1 et fichier2 – n’a absolument aucune importance puisqu’elle n’est jamais traitée. Elle n’est qu’un lien logique entre deux appels de procédure.

C’est un élément important : programmer la partie cliente d’un tel service ne nécessite pas plus de pré-requis techniques qu’il n’en faut pour la version « classique » illustrée plus haut pourtant bien moins fonctionnelle.

Evolutions côté service

Gestion des contextes

Pour permettre l’alimentation de plusieurs fichiers, nous pourrions transformer les variables globales du service en tableaux. La procédure ifstxt_nve() renverrait alors l’index de tableau qu’elle a utilisé pour créer le contexte et toutes les procédures le recevraient en paramètre. La procédure ifstxt_nve() réinitialiserait les entrées correspondant à cet l’index. Nous allons mettre en oeuvre une méthode tout aussi simple – mais bien plus souple – en utilisant des structures dynamiques.

Nous commençons par regrouper toutes les variables globales en une unique DS « modèle » : m_DS_IFSTXT. Vous remarquez que les champs ne sont plus initialisés au niveau de leurs déclarations.

     d m_DS_IFSTXT...
     d                 ds                  qualified based(null)
     d   hdlIfs...
     d                               10i 0
     d   nbrLigne...
     d                               10i 0
     d   nbrCarac...
     d                               10i 0
     d   txtErreur...
     d                               32a   varying

Note : à partir de la V6R1, vous pouvez remplacer « based(*) » par « template ».

Création du handle

La création du handle comprend trois étapes :

  • L’allocation d’un espace mémoire de la taille la DS « modèle » (en l’occurrence m_DS_IFSTXT)
  • L’initialisation de cet espace par l’intermédiaire d’une variable reposant sur le même modèle
  • Le renvoi de l’adresse de cet espace.

Dans notre solution d’alimentation de fichier, le handle est donc un pointeur vers l’espace mémoire réservé pour stocker le contexte. C’est ce pointeur qui passera de procédure en procédure et leur permettra de retrouver les informations persistantes propres à chaque contexte. Il remplace les variables globales.

Chaque fois qu’il sera nécessaire de créer un contexte, un espace mémoire lui sera réservé. Il est donc théoriquement possible d’alimenter une infinité de fichiers simultanément.

Ci-dessous, la procédure de création de handle ifstxt_nve(). Les couleurs permettent de mettre en évidence les deux éléments clefs :

  • en bleu : le pointeur contenant l’adresse de l’espace mémoire
  • en orange : la DS utilisé chemin d’accès pour mettre à jours cet espace
     p ifstxt_nve...
     p                 b                   export
     d                 pi              n
     d contexte_p...
     d                 s               *
     d contexte...
     d                 ds                  likeds(m_DS_IFSTXT) based(contexte_p)
      /free

       // Alloue l'espace nécessaire au stockage des données basées sur m_DS_IFSTXT
       contexte_p = %alloc(%size(m_DS_IFSTXT));

       // A partir de cet instant, l'espace alloué est modifiable par l'intermédiaire
       // de la DS qualifiée "contexte".

       // Initialise la DS.
       clear contexte;

       // Retourne le pointeur vers l'espace réservé. Du point de vue du client se sera le handle
       // vers la ressource sur laquelle il souhaite opérer
       return contexte_p;

      /end-free
     p                 e

Mise à jour des procédures

Par rapport à notre première version mono-instance, la logique de traitement de nos procédures ne change pas : seul l’accès aux informations persistantes évolue. Celles-ci, qui étaient mémorisées dans des variables globales, vont maintenant être stockées dans l’espace mémoire précédemment réservé par la procédure ifstxt_nve() et dont l’adresse sera passée à chaque procédure.

En résumé, ce ne sont plus les procédures qui choisissent où stocker les informations persistantes, mais l’appelant qui en désigne l’emplacement. Emplacement lui même initialisé par une procédure du même service.

Le code ci-dessous illustre ce mécanisme de récupération du contexte qui sera mis en oeuvre dans chacune des procédures du service.

     p une_procedure_generique...
     p                 b                    
     d                 pi               n  
     d   p_contexte_...
     d                                  *   value
     d contexte_...
     d                 ds                   likeds(m_DS_IFSTXT)
     d                                      based(p_contexte_)
      /free

       // Contrôle que p_contexte_ contient bien une adresse.
       if p_contexte_ = *null;
         return *off;
       endif;

       // A partir d'ici, les champs de  "contexte_" sont utilisables pour modifier 
       // l'espace mémoire.
       contexte_.txtErreur = 'Pas d''erreur...';

       // Lors du prochain appel à la procédure avec le même contexte nous retrouverons ces
       // informations.

       return *on;

      /end-free
     p                  e


Le paramètre p_contexte_ est le handle transmis par l’appelant. Nous avons vu dans la procédure ifstxt_nve() que ce handle est un pointeur vers un espace mémoire initialisé avec une data structure de type m_DS_IFSTXT.

Nous déclarons donc, dans notre procédure, une data structure contexte_,elle aussi de type m_DS_IFSTXT, que nous utilisons comme « chemin d’accès » à cet espace. Nous pouvons maintenant mettre à jour le contexte en question.

Sur le principe de la procédure générique présentée ci-dessus, voici la version handlerisée de ifstxt_ajtLigne() :

     p ifstxt_ajtLigne...
     p                 b                   export
     d                 pi
     d   p_contexte_...
     d                                 *   value
     d   txt_...
     d                            16384a   const varying
     d CRLF...
     d                 s              2a   inz(x'0D25') static
     d contexte_...
     d                 ds                  likeds(m_ds_IFSTXT)
     d                                     based(p_contexte_)
      /free

       // Contrôle d'usage
       if p_contexte_ = *null;
         return;
       endif;

       clear contexte_.txtErreur;

       // Contrôle d'usage
       if contexte_.hldIFS < 0;
         contexte_.txtErreur = 'Aucun fichier n''est ouvert.';
         return;
       endif;

       if contexte_.nbrLigne > 0;
         IFSWrite(contexte_.hldIFS: %addr(CRLF): 2);
         contexte_.nbrCarac += 2;
       endif;

       IFSWrite(contexte_.hldIFS: txt_: %len(txt_));
       contexte_.nbrCarac += %len(txt_);

       contexte_.nbrLigne += 1;

      /end-free
     p                 e

Libération du handle

Pour réserver l’espace mémoire, il a fallu utiliser la fonction intégrée %alloc(). Ces espaces doivent toujours être ensuite libérés avec dealloc(). Si ce n’est pas fait cela provoque une « fuite mémoire »: un espace mémoire est réservé mais son adresse est perdue. Dans un tel cas, seule la clôture du groupe d’activation du programme de service permettra de récupérer cet espace.

Dans notre service cette procédure de libération va avoir deux rôles : la clôture du fichier IFS et la libération de la mémoire.

     p ifstxt_lbr...
     p                 b                   export
     d                 pi              n
     d   p_contexte_...
     d                                 *   value
     d contexte_...
     d                 ds                  likeds(m_DS_IFSTXT)
     d                                     based(p_contexte_)
      /free

       if p_contexte_ = *null;
         return *on;
       endif;

       // Clôture le fichier si nécessaire
       if contexte_.hldIFS > 0;
         ifstxt_ferFichier(p_contexte_);
       endif;

       // Libère l'espace mémoire
       dealloc p_contexte_;

       return *on;

      /end-free
     p                 e

Sources

Les sources complets de ces exemples sont disponibles ici.

Autour des handles

Couplage faible, évolutions souples.

Cette méthode de handle permet de réaliser des couplages faibles entre les programmes de service et les programmes clients. Si nous voulions ajouter la possibilité d’obtenir le temps écoulé entre la création du document et sa clôture, nous aurions simplement à modifier la data structure modèle en y ajoutant deux champs timestamp : le premier renseigné dans la procédure ifstxt_creFichier() et le second dans la procédure ifstxt_ferFichier(). Nous créerions ensuite une procédure ifstxt_infTempsAlimentation() qui retournerait le delta entre ces deux champs.

     d m_DS_IFSTXT...
     d                 ds                  qualified based(null)
     d   hdlIfs...
     d                               10i 0
     d   nbrLigne...
     d                               10i 0
     d   nbrCarac...
     d                               10i 0
     d   txtErreur...
     d                               32a   varying
     d   tsDebut...
     d                                z
     d   tdFin...
     d                                z   

Notez que l’ajout de champs dans cette structure n’entraîne aucune modification sur les procédures existantes dans le service. Il faut simplement recompiler les modules qui l’utilisent. Au niveau des programmes client, cette modification n’a absolument aucun impact. Il en est de même avec la création de nouvelles procédures recevant ce handle : leurs créations ne nécessitent aucune intervention sur les programmes client déjà en exploitation.

Concluons avec un « mais »…

La méthode ne fonctionne pas dans environnement de service stateless. Un tel service n’est par exemple pas utilisable depuis un programme PHP. C’est clairement une technique à utiliser dernière un service.

Si jamais vous avez besoin de précisions, si vous souhaitez en apporter, ou même objecter, je suis à l’écoute !