[EN] [IT]
[INFO] [ACQUÉRIR] [PLAN] [RESSOURCES]
CHAPITRE
APPRENDRE À PROGRAMMER AVEC FUTUREBASIC
NOTES

Variables IV

Les handles
Les containers

Leçon 7 : Le cauchemar du programmeur

Les handles
Il faut savoir quʼavec les interfaces graphiques en particulier qui agrémentent les ordinateurs dʼaujourdʼhui, des blocs de mémoire sont continuellement alloués, pour, par exemple, stocker des valeurs relatives aux fenêtres, aux boutons et autres champs dʼédition, puis ces blocs sont libérés lorsque les fenêtres sont refermées. La mémoire est très souvent sollicitée et le mécanisme des pointeurs est relativement peu adapté à une gestion efficace de la mémoire.

Un autre mécanisme va prendre le relais pour corriger la déficience des pointeurs. Ce mécanisme peut être lʼallié des programmeurs dans la création dʼapplications sophistiquées, tout comme il peut devenir la source de bien des désagréments lorsquʼil est mal utilisé.

Lʼidée est la suivante : si les blocs de données pouvaient être réorganisés en mémoire, on pourrait alors éviter la fragmentation en blocs discontinus et utiliser au mieux lʼespace mémoire disponible.

Il faut pouvoir cependant garder la trace de lʼadresse de ces blocs amovibles, si lʼon veut accéder aux données quʼils contiennent. Ce résultat est obtenu en créant un pointeur qui contient lʼadresse dʼun autre pointeur. Le pointeur de pointeur est appelé handle et son contenu est alors géré automatiquement par le Memory Manager de la Toolbox. Lorsque le Memory Manager aura besoin de déplacer un bloc en mémoire, il mettra de lui-même à jour la valeur contenue dans le handle pour que celui-ci indique correctement lʼemplacement en mémoire du bloc quʼil est censé référencer. On accède alors aux blocs de manière indirecte en interrogeant dʼabord le handle pour connaître lʼemplacement des données à tout instant. Bien entendu, les risques de ranger des valeurs dans des zones de mémoire erronées sont augmentés, mais il existe des règles à respecter pour éviter les désagréments dʼun plantage qui ne manquerait pas dʼarriver : lorsquʼon crée des variables handles on prend vite lʼhabitude de tester la validité des adresses quʼils contiennent avant de manipuler les données et on « verrouille » très souvent les handles pendant le court instant où on doit y ranger des valeurs afin d'éviter quʼils soient déplacés en mémoire par le Memory Manager avant ou pendant les opérations qui vont modifier les données. De plus, lorsque les données ne sont plus utiles pour le programme, il faut libérer la mémoire occupée afin de permettre aux opérations ultérieures de se dérouler correctement. Un des avantages dans lʼutilisation des handles est de pouvoir gérer des blocs aussi grands que lʼespace en mémoire alloué à votre application le permet. Les blocs de mémoire peuvent être, de surcroît, retaillés au gré des besoins du programme beaucoup plus facilement que les blocs fixes référencés par des pointeurs (les blocs fixes et les blocs amovibles sont créés dans des zones différentes de l'espace mémoire alloué à l'application). .

Les handles sont par essence plus difficiles à gérer que des variables traditionnelles. La difficulté est accrue du fait quʼil existe deux types de handles qui, bien que fonctionnant sur des principes quasiment identiques, demandent des traitements différents. Sur le Macintosh certains handles sont gérés par le Memory Manager (gestionnaire de mémoire) et dʼautres le sont pour partie par le Resource Manager (gestionnaire de ressources).

Le concept de ressource est un concept essentiel au Macintosh tel que nous lʼavons connu jusquʼici, et nous verrons ultérieurement ce que cela recouvre, il faut juste savoir pour le moment que certains handles peuvent être créés directement par vous et que dʼautres le sont par le Resource Manager associé au Memory Manager. Selon lʼorigine du handle que vous avez à manipuler vous pouvez avoir des traitements différents à effectuer.

Les handles sont très puissants, mais leur apparente complexité, a fait qu'on évite génralement de les introduire dans les langages pour débutants. On peut bien sûr écrire avec FutureBASIC des programmes sans déclarer explicitement un seul handle, ce qui ne veut pas dire que votre application nʼen manipulera aucun. Bien au contraire, il est probable que des handles peuplent par dizaines la mémoire allouée à votre application sans que vous en soyez conscient, le Runtime se chargeant de la gestion des handles dont il a besoin pour accomplir ses tâches efficacement.

Voici une représentation possible de la mémoire impliquant une variable de type handle :

DIM monHandle AS HANDLE
monHandle = FN NewString("Hello")

Le bout de code ci-dessus déclare une variable handle et appelle la fonction de la Toolbox NewString qui réserve un bloc relogeable en mémoire et y stocke la chaîne Pascal qui lui est passée en paramètre.

Variable Adresse Hexadécimale
monHandle 056E3F80 056E3A00  
056E3F81
056E3F82
056E3F83
       
(pointeur) 056E3A00 056EF0B0  
056E3A01
056E3A02
056E3A03
       
Décimale ASCII
(bloc relogeable) 056EF0B0 5  
056EF0B1 72 H
056EF0B2 101 e
056EF0B3 108 l
056EF0B4 108 l
056EF0B5 111 o


Le contenu de la variable monHandle (si l'opération a réussi) est une adresse en mémoire (en rouge dans le tableau). Les adresses-mémoire sont codées sur des entiers longs (occupant 4 octets) représentées généralement en valeurs hexadécimales.

À l'adresse trouvée dans la variable handle est rangée une autre adresse-mémoire (en bleu dans le tableau). Cette autre adresse indique le début du bloc en mémoire où l'on peut retrouver les octets qui composent notre chaîne.

Pour un débutant, travailler avec les handles est beaucoup plus périlleux. L'expérience est cependant grisante, mais c'est comme si on jouait dans la cour des grands, l'espace est plus grand mais les contacts plus rugueux. Une erreur vous conduit très rapidement au crash du programme quand ce n'est pas de la machine toute entière. Le mécanisme est pourtant très éprouvé et en suivant les règles de bonne conduite, il n'y a aucune raison que les choses se passent mal.

Lorsqu'on utilise les handles, on doit collaborer avec le Memory Manager qui a de son côté d'importantes tâches à régler.

Une des règles essentielles est de toujours s'informer auprès du Memory Manager pour savoir s'il a pu faire son travail dans de bonnes conditions. Lorsqu'on lui demande de réserver un bloc en mémoire, on doit toujours s'inquiéter de savoir si notre requête a abouti avant de faire quoi que se soit. La plupart des appels à la Toolbox qui retournent des handles, renvoient la valeur 0 quand l'opération a échoué, la vérification dans ce cas-là consiste à tester la valeur de notre variable handle, si elle est non nulle nous avons alors obtenu un bloc valide, si elle est nulle, cela signifie dans la majorité des cas que la mémoire est saturée et que le bloc n'a pu être alloué, et bien sûr notre programme doit alors prendre les mesures qui s'imposent.

Vous verrez très souvent dans les extraits de code que vous étudierez le genre de séquence suivante :

DIM monHandle AS HANDLE
monHandle = FN NewString("Hello")

LONG IF
monHandle <> _nil
// On a obtenu le handle
XELSE
// Le handle n'a pu être créé
END IF


Dans une situation d'échec, souvent due au manque d'espace-mémoire disponible, on peut vouloir informer l'utilisateur du problème rencontré, afin de lui suggérer des mesures à prendre, comme quitter le programme pour augmenter, dans le Finder, la mémoire allouée à l'application. Un problème peut cependant survenir ici, car vous pourriez ne pas avoir suffisamment de mémoire disponible pour afficher même ce type d'information. Certains programmeurs ont pris l'habitude d'allouer au démarrage de leur programme un bloc tampon suffisamment grand pour régler cette possible difficulté et lorsque la mémoire vient à manquer, ils libèrent ce bloc pour permettre l'affichage du message d'alerte. D'autres programmeurs évaluent l'espace disponible avant d'exécuter des fonctions dont ils savent qu'elles vont réclamer une bonne quantité de mémoire.

À la différence des variables traditionnelles qui sont créées et détruites grâce au code généré par le Compilateur, les handles sont gérés par le Memory Manager et par le programmeur. Le Memory Manager essaie de rentabiliser au mieux l'espace-mémoire alloué à votre application, si par exemple l'espace vient à manquer pour réserver ou agrandir un bloc, il va réorganiser les blocs relogeables de telle sorte qu'ils soient contigus en mémoire éliminant ainsi les espaces entre les blocs qui sont inutilisés (on verra aussi qu'il peut être autorisé à retirer de la mémoire certains blocs momentanément inutiles). Si ce compactage ne permet toujours pas de réserver le bloc, le Memory Manager vous signalera l'erreur rencontrée soit en retournant un handle nul, soit en positionnant un code d'erreur que vous pouvez consulter grâce à la fonction de la Toolbox MemError. Lorsque le Memory Manager déplace un bloc, il met à jour le pointeur qui le référence de telle sorte que l'adresse indiquée reflète le nouvel emplacement du bloc en mémoire. Ce mécanisme est parfaitement rôdé, mais il peut vous conduire à commettre des erreurs fatales à votre application. Considérez l'exemple suivant :

DIM monHandle   AS HANDLE
DIM autreHandle AS HANDLE
DIM monPointeur AS POINTER

monHandle = FN NewHandle(100000)
LONG IF monHandle
  monPointeur = [monHandle]
  autreHandle = FN NewHandle(100000)
  LONG IF autreHandle = _nil
    PRINT "Mémoire insuffisante !"
  END IF
  & monPointeur, 1215
XELSE
  PRINT "Mémoire insuffisante !"
END IF


Le code ci-dessus contient un bug parfois difficile à traquer, car il peut fonctionner très longtemps avant qu'un imprévu ne survienne.

Un nouvel handle est demandé au Memory Manager pointant sur un bloc de 100000 octets. Si l'opération a réussi, le code extrait l'adresse du bloc relogeable avec, ici, le raccourci utilisant les crochets (on aurait pu utiliser une syntaxe plus classique avec la fonction PEEK LONG qui retourne un entier long depuis l'adresse-mémoire qui lui est passée en paramètre). Lorsqu'on extrait le pointeur d'un handle, on dit que l'on déréférence le handle.

Ensuite un autre handle de 100000 octets est réclamé au Memory Manager. Enfin, un entier long ayant pour valeur 1215 est rangé dans le premier bloc mémoire alloué. Là encore on aurait pu utiliser la syntaxe plus classique POKE LONG.

Un problème peut survenir lorsque le Memory Manager voudra allouer le second bloc : si la mémoire n'est pas suffisante, il va procéder à des déplacements de blocs relogeables y compris le bloc réservé pour notre premier handle, si bien que notre variable appelée monPointeur qui contient une copie de l'adresse originale, ne contiendra plus l'adresse valide pour retrouver notre premier bloc. L'écriture en mémoire de la valeur 1215 peut alors avoir des conséquences fâcheuses, qui sait ce que peut contenir cette zone mémoire à cet instant ?

Pour éviter ce genre de désagrément on peut verrouiller le bloc à l'aide de la procédure de la Toolbox HLock (le Memory Manager ne déplace pas les blocs qui lui sont signalés comme verrouillés). Moralité, si vous devez déréférencer un handle, faites-le une fois que le bloc aura été verrouillé en mémoire. Il est recommandé également de garder ses blocs verrouillés le moins longtemps possible pour que le Memory Manager puisse travailler confortablement.

La manipulation des handles vous oblige à adopter des stratégies. L'exemple prétexte proposé pourrait éventuellement s'écrire de la manière suivante afin de sécuriser au maximum les traitements :

DIM monHandle   AS HANDLE
DIM autreHandle AS HANDLE
DIM monPointeur AS POINTER

monHandle = FN NewHandle(100000)
LONG IF monHandle
  autreHandle = FN NewHandle(100000)
  LONG IF autreHandle = _nil
    PRINT "Mémoire insuffisante !"
  END IF
  HLock(monHandle)
  LONG IF FN MemError = _noErr
    monPointeur = [monHandle]
    & monPointeur, 1215
  END IF
  HUnlock(monHandle)
XELSE
  PRINT
"Mémoire insuffisante !"
END IF

Vous pourriez aussi bien écrire ceci :

DIM AS HANDLE monHandle,autreHandle

monHandle = FN NewHandle(100000)
LONG IF monHandle
  autreHandle = FN NewHandle(100000)
  LONG IF autreHandle = _nil
    PRINT "Mémoire insuffisante !"
  END IF

  & [monHandle], 1215
XELSE
  PRINT "Mémoire insuffisante !"
END IF


Notez également que lorsque vous écrivez ceci :

autreHandle = monHandle

Vous copiez simplement l'adresse contenue dans la variable monHandle dans la variable autreHandle, en aucun cas vous n'avez dupliqué les données contenues dans le bloc relogeable référencé. Vous avez en quelque sorte créé un alias de votre handle original, mais attention, cet alias restera valide pour autant que le Memory Manager ne déplace pas le bloc original, car n'ayant pas été directement sollicité, il n'a aucun moyen de savoir que votre code a dupliqué l'adresse contenue dans une variable handle et il ne mettra donc pas à jour cette copie si le bloc référencé venait à être déplacé.

Pour dupliquer un handle avec ses données vous pouvez soit  :

  • créer un nouvel handle et copier les octets du premier bloc vers le second (note: la fonction de la Toolbox GetHandleSize retourne la taille du bloc pointé par le handle qui lui est passé en paramètre) :

    LONG IF monHandle
      autreHandle = FN NewHandle(FN GetHandleSize(monHandle))
      LONG IF autreHandle
        BLOCKMOVE [monHandle],[autreHandle], FN GetHandleSize(monHandle)
      XELSE
        PRINT "Mémoire insuffisante !"
      END IF
    END IF


  • utiliser une fonction de la Toolbox qui se chargera de créer le bloc et copier les données en une seule opération ou presque :

    LONG IF monHandle
      autreHandle = monHandle
      LONG IF FN HandToHand(autreHandle) <> _noErr
        PRINT "Mémoire insuffisante !"
      END IF
    END IF


    En tant que programmeur, vous êtes responsable des blocs que vous allouez au moyen de handles, et vous devez vous assurer de leur libération. Une bonne pratique à adopter est la suivante : aussitôt que vous avez besoin de créer un handle dans une partie de votre programme, trouvez dans votre code source où le bloc devenu inutile doit être libéré et écrivez immédiatement les instructions pour libérer le bloc. Les blocs sont libérés au moyen de la commande DisposeHandle. Cette commande ne remettra pas votre variable à 0 et vous devez le faire vous-même:

    LONG IF monHandle
      DisposeHandle(monHandle)
      monHandle = _nil
    END IF


    FutureBASIC propose l'instruction DEF DISPOSEH(monHandle) qui libérera le bloc et remettra la variable passée en paramètre à 0, en supplément elle vérifiera la nature du handle et ne libèrera pas le bloc s'il s'agit d'un handle sur une ressource. Cette commande nécessite que la variable handle soit en mémoire adressable, nous verrons ce que cela veut dire ultérieurement, mais c'est un léger inconvénient.

    Nous n'avons pas encore vu le concept génial de ressource, mais sachez que les ressources sont des données stockées sur le disque auxquelles votre programme peut accéder et qui peuvent être chargées en mémoire dans des blocs référencés par des handles. La gestion de ces handles diffère quelque peu de celle des handles réguliers.

    En réalité, les handles peuvent être aisément maîtrisés si l'on respecte un petit nombre de règles :

  • Vérifiez systématiquement la validité des handles avant de faire quoi que ce soit avec, de même, vérifiez que les fonctions de la Toolbox se sont bien déroulées.

  • Avant même de penser à leur manipulation, pensez à la libération des handles que vous devez créer.

  • Verrouillez momentanément un handle si la mémoire est susceptible d'être réorganisée par le Memory Manager pendant que vous devez lire ou écrire les données dans le bloc relogeable qu'il référence.

    Le respect de ces trois règles très simples vous évitera bien des déconvenues et des frustrations.

    Traditionnellement, le BASIC ne met pas les handles à la disposition des programmeurs, ces entités étant considérées difficiles à manipuler, FutureBASIC fait cependant une entorse au langage en proposant des routines utilisateurs pour créer, verrouiller, déverrouiller et libérer des blocs relogeables :

    monHandle = USR _allocRelBlk(taille&)
    USR _lockBlk(monHandle)
    USR _unlockBlk(monHandle)
    USR _disposeBlk(monHandle)

    Cependant, nous préférerons ici, nous en tenir aux fonctions et procédures de la Toolbox que l'on retrouve identiques dans d'autres langages.

    Le problème que vous serez amené à rencontrer est lié à la documentation des fonctions disponibles; en effet, la vocation de FutureBASIC n'est pas de dupliquer les informations contenues dans Inside Macintosh (la documentation d'Apple). Cette documentation, qui peut être librement téléchargée depuis le site web d'Apple, est composée de plusieurs dizaines de volumes au format PDF détaillant tous les Managers de la Toolbox, leurs constantes, leurs records et leurs fonctions ainsi que d'importantes indications sur leur utilisation. Lorsque la curiosité ou la nécessité vous conduira à consulter cette documentation, vous comprendrez pourquoi on l'appelle communément la Bible des programmeurs Macintosh. Le mécanisme mis en place dans FutureBASIC^3 vous permet virtuellement, à la différence d'autres langages pour débutants, l'accès à la totalité des fonctions et procédures de la Toolbox comme s'il s'agissait de mots-clés intégrés au langage.

    Tout comme les pointeurs, les handles peuvent être typés, ce qui nous permet dʼindiquer au Compilateur le type de variable que nous entendons stocker dans les blocs relogeables en mémoire que nous avons réservés  

    DIM unHandle AS HANDLE TO RECT

    Lʼinstruction ci-dessus déclare une variable de type handle qui pointera sur un bloc de la taille dʼune structure RECT. Notez les deux variantes de lʼinstruction ci-dessus que vous pouvez être amené à rencontrer :

    DIM unHandle AS HNDL TO RECT
    DIM unHandle AS ^^RECT

    unHandle = FN NewHandle(8)

    Ci-dessus, avec la fonction de la Toolbox NewHandle, on demande au Memory Manager de réserver un bloc de 8 octets et de nous retourner un handle sur ce bloc. Le nombre 8 représente la taille en octets du bloc que vous souhaitez allouer, dans notre exemple, cʼest la taille dʼun record de type RECT. Il nʼest pas toujours pratique de passer une valeur littérale à la fonction NewHandle, car vous pourriez, par exemple, vouloir réserver des blocs pour des records plus compliqués. FutureBASIC fournit une fonction bien utile à laquelle vous passez en paramètre une variable (ou un type de variable) et elle vous retourne la taille en octets occupée par la variable (ou les variables de ce type). Il est plus judicieux dʼutiliser une telle fonction. On écrirait donc plus volontiers ceci :

    unHandle = FN NewHandle(SIZEOF(RECT))

    Ci-dessus, le résultat de la fonction SIZEOF est passé en paramètre à la fonction de la Toolbox NewHandle.

    Lorsque le handle est créé, on peut accéder alors aux champs du rectangle comme ceci :

    unHandle..top = 20
    unHandle..left = 30


    Notez les deux points qui signifient la double indirection.

    DisposeHandle(unHandle)

    Ci-dessus, la procédure de la Toolbox DisposeHandle nous permet de libérer la mémoire occupée par le bloc.

    Attention : dans le code simplifié pour illustrer les propos, aucune précaution n'est prise pour tester la validité du handle. Sachez que, pour une grande part, lʼinstabilité de certains programmes sur Macintosh provient dʼune mauvaise gestion des handles et des problèmes de mémoire que cela engendre.

    Les containers
    Pour faciliter la vie aux programmeurs, FutureBASIC^3 a introduit un type de variable un peu spécial : les containers. Les containers (suffixe $$) sont des blocs de mémoire capable de contenir théoriquement jusquʼà 2 gigaoctets de données. Ils peuvent servir, par exemple, à manipuler des chaînes de caractères dʼune taille supérieure à 255 octets tout en autorisant lʼutilisation de fonctions qui sont en principe adaptées au traitement des chaînes Pascal.

    On peut également, y ranger des valeurs numériques.

    Les containers sont en réalité des handles déguisés, dont la gestion est simplifiée pour le débutant. Exemple:

    DIM monContainer AS CONTAINER
    monContainer = "Une chaîne de caractères"


    Lʼinstruction ci-dessus range une chaîne dans le container monContainer.

    monContainer = ""

    Lʼinstruction ci-dessus est la manière adéquate pour libérer la mémoire occupée par les données du container.

    monContainer = 10

    Lʼinstruction ci-dessus range la valeur 10 dans la variable container.

    monContainer = monContainer + 20

    On peut éventuellement exécuter des opérations avec les valeurs contenues dans un container. Ci-dessus la valeur contenue dans le container sera additionnée de 20.

    Les containers, comme on le voit, sont très simples à manipuler, en fait le travail de gestion des handles nécessaires pour ce type de variables est réalisé essentiellement par le Runtime. Cependant, les containers souffrent dʼune limitation dont on reparlera plus tard.



    [Precédent] [Table des Matières] [Suivant]
  • {Note}
    © 2000 Pix&Mix
    Tous droits réservés

    FutureBASIC est une marque déposée appartenant à Staz Software, Inc et utilisée avec permission.