Les objets légers : un concept pour optimiser les classes vb

Les objets légers : un concept pour optimiser les classes vb

Rappel sur la pile et le tas

La pile sert à stocker temporairement des informations nécessaires à l'exécution des fonctions. C'est, en général, le compilateur qui gère la pile.

Le tas sert à stocker toute donnée qui doit être allouée pendant l'exécution du programme et qui n'est pas connu à la compilation (en principe). C'est le système d'exploitation qui alloue cette mémoire.

VB se sert de la pile pour les tableaux et les chaînes principalement.

Le concept d'objet "léger"

D'abord, il faut savoir qu'en plus de la lourdeur de la gestion de l'interface IDispatch, tout module de classe utilise, au moins, 96 octets de mémoire (sans compter le code) à la base. Ce qui fait beaucoup quand on pense que (hormis le code des méthodes) la structure minimum d'un objet qui ne supporte que IUnknown est :

'le contenu de la vtable

Private Type VTable
    Table(0 To 2) As Long
End Type

Private Type Objet
    'le pointeur vers la vtable 
    pVTable as long
    'un compteur d'instance interne            
    cRefCountas long
End type

Ce qui nous fait une taille de 4 + 4 = 8 octets. On ne compte pas la vtable car elle est commune à toutes les instances de l'objet.

Ensuite, il faut savoir que, bien que VB n'ai pas de type correspondant à IUnknown, il est tout à fait capable de gérer cette interface sans IDispatch. On peut donc déclarer des interfaces à base de IUnknown dans une typelib et les implémenter dans VB.

Un objet léger à n méthodes sera donc un objet qui, une fois instancié, paraîtra comme un objet normal. La seule différence est qu'il ne fera que 8 + la taille des données octets pour une instance.

L'implémentation d'un objet léger

Eh bien là ça n'est pas si simple. Vous allez me dire "C'est simple, une typelib et un Implement et le tour est joué". Eh bien non ! Un objet léger ça se gère dans un module BAS et avec une typelib .

Vous déclarez votre interface en langage ODL, vous compilez avec mktyplib.exe (pas avec MIDL.exe) et cela vous donne votre typelib. Vous devez définir toutes vos interfaces (même celles déjà existantes, comme IEnumVARIANT). Voir le tutorial sur les bibliothèques de types.

Dans votre projet,vous devez ajouter une référence à la bibliothèque de type créée.

Dans un module BAS,vous définissez une fonction, disons InitObjet, du type de l'interface avec les paramètres pour les données initiales de l'objet (qui seraient passées au constructeur en C++). Dans cette fonction, il faut :

  • Initialiser la vtable si ce n'est pas déjà fait,
  • Initialiser la structure de l'objet pour la vtable et le nombre de références à 1,
  • Copier l'adresse de la structure dans la variable InitObjet avec CopyMemory

La gestion des objets légers dans le tas

La structure de l'objet sera allouée dans le tas, c'est à dire dans une zone de mémoire allouée dynamiquement par le système d'exploitation. Il faudra don contrôler la fonction Release pour libérer la mémoire au bon moment.

Il nous faudra les déclarations suivantes :

Private Type VTable
    Methods(0 To 2) As Long
End Type

Private m_pVTable As Long

Private m_VTable AsVTable

Private Type typObjetHeap
    pVTable As Long
    cCount As Long

    'données attachées
    '--------------------
    '...
End Type

Toutes les définitions sont privées car la gestion de la structure se fait en interne. On expose uniquement l'interface.

Notons les points suivants :

  • la première structure Vtable contient seulement un tableau. On pourrait se dire que l'on pourrait simplifier en déclarant simplement un tableau mais les tableaux sont libérés avant les structures à la fin de l'exécution. Il pourrait alors arriver que la vtable soit libérée avant l'objet, ce qui conduirait à un crash. La variable globale m_VTable sera donc détruite assurément après l'objet.
  • La variable globale m_pVTable sert à connaître l'adresse de la vtable et ainsi savoir si elle a été initialisée.
  • la structure typObjetHeap est la structure d'un objet en mémoire. Elle comprend pVTable, le pointeur vers la vtable, et cCount, le compteur d'instances. Dans l'exemple je n'ai pas repris les données de l'objet.

Voici,maintenant, le code pour initialiser un objet (notez que cette construction apporte plus que New et donne un équivalent des constructeurs du C++) :

Public Function FuncPtr(ByVal addr As Long) As Long
   FuncPtr  = addr
End Function

Public Function InitObjetHeap() As <"nom_interface">
    'pointeur vers la structure de l'objet
    Dim ptrObjet As Long
    'contenu de l'objet
    Dim Objet As typObjetHeap

    'si la vtable n'est pas initialisée
    If m_pVTable =  0 Then
        'on la remplit
        With m_VTable
            'IUnknown
           .Methods(0) = FuncPtr(AddressOf QueryInterface)
           .Methods(1) = FuncPtr(AddressOf AddRef)
           .Methods(2) = FuncPtr(AddressOf Release)
           'initialisation des autres méthodes de l'interface à implémenter dans l'ordre de définition dans la typelib
        End With

        'et on en garde l'adresse
        m_pVTable = VarPtr(m_VTable)
    End If

    'on contruit l'objet
    With Objet
        'le pointeur vers lavtable
        .pVTable = m_pVTable
        'le compteur de référence : on crée un objet donc il est à un
        .cCount = 1
        'initialisation des autres données de l'objet
        '...
    End With

    'on alloue de l'espace mémoire pour l'objet
    ptrObjet = CoTaskMemAlloc(LenB(Objet))
  
    'si succès
    If ptrObjet Then
        'on remplit l'objet
        CopyMemory ByVal ptrObjet, Objet, LenB(Objet)
    End If

    'on assigne la référence à la variable de retour de la fonction
    CopyMemory ByVal VarPtr(InitObjetHeap), ptrObjet, 4&

    ZeroMemory Objet, LenB(Objet)
End Function

Notons les points suivants :

  • Il nous faut une variable temporaire pour stocker la structure de l'objet avant de la transférer dans la zone mémoire allouée à cet effet.
  • Nous ne pouvons pas affecter l'adresse d'une fonction, obtenue avec AddressOf, directement à une variable. Il faut donc une fonction qui renvoie le paramètre qu'on lui passe. Si la vtable n'est pas initialisée, on remplit le tableau correspondant.
  • Nous initialisons la structure de l'objet : on pointe la vtable et on met à 1 le nombre d'instance puisque l'objet renvoyé est une instance.
  • Nous allouons une zone mémoire dans le tas, puis nous copions l'objet dans cette zone.
  • Comme une variable de type objet (ou interface) est un pointeur, nous copions l'adresse de la structure dans le tas, dans la variable de retour de la fonction.
  • Le ZeroMemory est nécessaire pour que les éventuelles instances d'objets présentes dans la structure de l'objet ou des tableaux ne soient pas supprimées du fait de la destruction de la structure temporaire.


Utilisation
:

On écrira le code suivant :

Dim objet As <interface>

Set objet = InitObjetHeap

'l'utilisation

Set objet = Nothing

La gestion des objets légers dans la pile

La structure de l'objet sera allouée dans la pile, par le biais d'une variable locale. Il faudra donc veiller à ne pas utiliser l'objet en dehors de la portée de la fonction dans laquelle est déclarée la variable locale. Sinon, on risque de libérer la vtable avant l'appel à Release ce qui produit un crash de VB ou de l'exe.

Note : il faut toujours déclarer la variable du type de l'interface de l'objet AVANT la structure afin de ne pas libérer la structure avant l'objet. Il est aussi nécessaire de faire Set variable_objet = Nothing à la fin de la fonction.

Public Type typObjetStack

    'le pointeur vers la vtable
    pVTable AsLong
    
    'données attachées
    '--------------------
    '...

End Type

Notons que, cette fois, la structure est publique puisqu'il faudra déclarer une variable locale de ce type pour stocker l'objet (en plus de la référence d'objet).

Public Function InitObjetStack(ByRef lpStruct As typObjetStack, ByVal cMaxSize As Long) As <nom_interface>

    Dim ptrObjet As Long
    
    'si la vtable n'est pas initialisée
    If m_pVTable = 0 Then
        'on la remplit
        With m_VTable
            'IUnknown
            .Methods(0) = FuncPtr(AddressOf QueryInterface)
            .Methods(1) =FuncPtr(AddressOf AddRefRelease)
            .Methods(2) = FuncPtr(AddressOfAddRefRelease)
            'Le reste des méthodes de l'interface
        End With

        'et on en garde l'adresse
        m_pVTable = VarPtr(m_VTable)
    End If

    'on construit l'objet
    With lpStruct
        'le pointeur vers la vtable
        .pVTable = m_pVTable
        'on remplit le reste des membres de l'objet        
    End With
 
    ptrObjet = VarPtr(lpStruct)
    'on assigne la référence à la variable de retour de la fonction
    CopyMemory ByVal VarPtr(InitObjetStack), ptrObjet, 4&
End Function

Notons les points suivants :

  • La structure est publique puisqu'il faut passer une variable de ce type en paramètre pour instancier l'objet.
  • On n'alloue plus d'espace mémoire dans le tas : on se sert de l'espace mémoire de la variable locale passée en paramètre.
  • On copie l'adresse de la variable dans la référence d'objet.

Utilisation :

On écrira le code suivant :

Dim structObjet As typObjetStack 
Dim objet As <interface>

Set objet  = InitObjetStack(structObjet)

    'l'utilisation

Set objet = Nothing

Gérer les méthodes de l'interfacede base IUnknown

Si le constructeur renvoie un objet du type de l'interface

La méthode QueryInterface de l'interface IUnknown est codée comme suit SI la fonction constructeur renvoie un type "interface de l'objet" :

'cette fonction sert à demander à l'objet s'il sait gérer l'interface iid 
    '(c'est un GUID)
'normalement VB n'appelle jamais QueryInterface
'puisque l'on assigne à une variable du type de l'interface 
'et que l'on ne supporte (à part IUnknown) qu'une seule interface

Private FunctionQueryInterface( _
  ByRef This As typObjet, _
  ByVal iid As Long, _
  ByRef ppvObject As Long_
) As Long

    'on se contente de refuser l'interface
    ppvObject  = 0
    QueryInterface =  E_NOINTERFACE
End Function

Voyons donc QueryInterface dans ce cas...
Dans l'utilisation normale des objets légers, cette méthode ne devrait jamais être appelée. En effet, on ne peut pas réellement affecter le retour de la fonction constructeur (InitObjet) à une variable Object, puisque les interfaces que nous pouvons implémentons ne supportent pas IDispatch. Nous nous contenterons donc de ne pas retourner de référence d'objet et renvoyer E_NOINTERFACE pour signaler à VB que l'on ne veut pas de QueryInterface.

Si la fonction constructeur renvoie un IUnknown

Si la fonction constructeur renvoie un IUnknown on codera QueryInterface comme suit :

Private Function QueryInterface( _
  ByRef This As typObjet, _
  ByVal iid As Long, _
  ByRef ppvObject As Long_
) As Long

If This.cCount > 1 Then
    ppvObject  = 0
    QueryInterface =  E_NOINTERFACE
Else
    This.cCount = This.cCount + 1
    ppvObject = VarPtr(This)
    QueryInterface = 0
End If
End Function

Dans ce cas, il faut autoriser un seul QueryInterface pour l'affectation à la variable qui va référencer l'objet. Pour cela, on regarde s'il y a moins de deux instances d'objet. Si oui, on incrémente le nombre de référence et on renvoie l'adresse de This (la référence) dans ppvObject. Puis il faut refuser tout autre cast pour éviter les erreurs de cast que l'objet ne supporte pas. Dans ce cas, on met 0 dans ppvObject et on renvoie E_NOINTERFACE pour dire que l'on ne veut pas de ce cast.

AddRef et Release pour les objets gérés dans le tas

Voyons, maintenant, AddRef et Release :

'cettefonction incrémente un compteur de référence 
'(nombre d'instance) de l'objet

Private Function AddRef(ByRef This As typObjetHeap) As Long

This.cCount = This.cCount + 1
AddRef = This.cCount

End Function

'cette fonction décrémente un compteur de référence 
'(nombre d'instance) de l'objet
'quand le compteur atteint 0, sa structure est libérée

Private FunctionRelease(ByRef This AstypObjetHeap) As Long

This.cCount = This.cCount - 1
Release = This.cCount
'Si l'on est dans le cas des objets sur pile, on n'a pas besoin de ce qui suit.

If This.cCount = 0 Then
    'on libère éventuellement les ressources allouées pour l'objet
    '...
    'et celle de l'objet
    CoTaskMemFree ByVal VarPtr(This)
End If
End Function

AddRef et Release sont complémentaires. VB appelle AddRef pour dire qu'il ajoute une référence de l'objet (un pointeur). VB appelle Release à chaque fois qu'une variable du type de l'interface sort de sa portée ou qu'elle reçoit Nothing. Pour la première fonction, on incrémente un compteur (et on renvoie le compteur, valeur qui n'est pas utilisée). Pour la seconde,on décrémente le compteur (et on renvoie aussi le compteur).

Dans le cas des objets alloués dans le tas, lorsque le compteur atteint 0,on doit libérer la structure de l'objet que l'on a allouée dans le tas. Ceci est impératif pour ne pas avoir de fuites de mémoire.

AddRef et Release pour les objets gérés sur la pile

Dans le cas des objets alloués dans la pile, on n'a rien à faire puisque la mémoire est libérée automatiquement à la fin de la procédure qui contient la variable locale.

Private Function AddRefRelease(ByVal This As Long) As Long

'un commentaire est nécessaire afin d'empêcher VB de supprimer la fonction

End Function

Gérer les autres méthodes personnelles

Le pointeur This

Toute méthode d'un objet reçoit implicitement un premier paramètre This qui est un pointeur vers la structure de l'objet pour lequel la méthode vient d'être appelé. Il est du type de la structure de l'objet,dans notre cas, il s'agit de typObjetHeap .

La valeur de retour

Il y a deux cas à prendre en compte :

  • Gestion d'erreur minimale,
  • Pas de gestion d'erreur.

Le troisième mode de gestion d'erreur est la gestion d'erreur riche. Lorsqu'une erreur est lancée,VB essaie de demander une interface Isupport ErrorInfo à QueryInterface de l'objet qui a généré l'erreur. Le problème est que cette interface, libérée après tous les modules dans notre vtable, n'existerait plus, donc crash. Il fautdonc toujours refuser cette interface dans QI.

Gestion minimale d'erreur

Le premier niveau de gestion d'erreur sous VB se fait par le type HRESULT que vous ne pouvez pas utiliser vous même. C'est l'équivalent d'un Long. Dans les modules de classe, la valeur de retour des fonctions est toujours un HRESULT. Mais alors, me direz vous, comment VB retourne-t-il une valeur à l'appelant ?

Eh bien, il utilise un paramètre qui se trouve à la fin de la liste des paramètres. Ce paramètre est de type pointeur et possède les attributs [out,retval] dans le fichier ODL. Dans VB, il sera déclaré comme un ByRef As <type>.

Et Alors dans le HRESULT, on met quoi ?
Le code d'erreur bien sur ! Il faut savoir que VB et l'objet Err transposent les codes d'erreur dans la plage &H0001 à &HFFFFet qu'ils transposent plusieurs codes d'erreur en un seul de l'objet Err. Il se trouve que toutes les erreurs de VB se retrouvent dans les valeurs de HRESULTdans la plage &H800A0001 à &H800AFFFF. On peut alors renvoyer le code d'erreur VB (par exemple 7 "Mémoire insuffisante") que l'on Or-era avec &H800A0000. Bien sur, s'il n'y a pas d'erreur, on renvoie 0.

C'est le principe debase de la gestion d'erreur dans VB.

Dans le langage ODL, on aura des méthodes définies comme suit :

HRESULT <nom méthode> (<paramètres>) ;

Pas de gestion d'erreur

Bien que VB s'attende à un HRESULT, on peut passer directement la valeur de retour dans la valeur de retour si la taille est inférieure ou égale à 8 octets. Sinon, il y a un paramètre supplémentaire qui est toujours passé en tout premier paramètre (avant le pointeur this). Il contient un pointeur vers la zone mémoire pour stocker le résultat.

Si l'on retourne une valeur dans le retour de fonction, on n'aura pas la possibilité de renvoyer un code d'erreur.

Dans le langage ODL, on aura des méthodes définies comme suit :

<type non pointeur> <nom méthode> (<paramètres>) ;

Si l'on n'a ni besoin de valeur de retour, ni de code d'erreur, on remplacera <type non pointeur></type> par void.

Les différents types de paramètres

Tous les types de bases : Byte, Integer, Long, Single, Double, Boolean, Currency, Date, Enum sont des types qui peuvent être passés directement par valeur. Pour ces types, si le paramètre est défini :

  • [in] <type>. C'est un ByVal As <type>
  • [in,out] <type>*. C'est un ByRef As <type>

<type> est à choisir parmi un nom d'enum, unsigned char,short, long, float, double, boolean, currency, DATE.

Il n'y pas d'autres combinaisons.
A noter que pour ces types, on peut définir une valeur par défaut, qui rend le paramètre optionel, avec léattribut defaultvalue(<valeur>).

Les types String (BSTR, LPSTR, LPWSTR), Object (IDispatch*) et autres types objets (toutes les interfaces) sont, par définition, des types pointeurs. Pour ces types, on définit :

  • [in] BSTR, [in] LPSTR, [in] LPWSTR. C'est un ByVal As String
  • [in,out] BSTR*, [in,out] LPSTR*, [in,out] LPWSTR*. C'est un ByRef As String
  • [in] IDispatch*. C'est un ByVal As Object
  • [in,out] IDispatch**. C'est un ByRef As Object

A noter que ByRef As Object (ou As <interface>) n'est nécessaire que lorsque l'on compte modifier la référence d'objet,ou en renvoyer une. Pour un simple passage de paramètre à des fins d'utilisation, on optera toujours pour un ByVal

Les types tableaux sont eux aussi des pointeurs :

  • [in] SAFEARRAY(<type>)* et [in,out] SAFEARRAY(<type>)* sont équivalents puisque l'on ne peut pas passer de tableau par valeur.

<type> est n'importe quel type, simple, structure ou tableau.

Les paramètres out,retval

Le dernier paramètre de la liste peut être attribué avec [out,retval] s'il est du type ByRef c'est à dire pointeur (long*, boolean*, ...,BSTR*, IDispatch**, **, ...). Il ne peut y en avoir qu'un seul. Dans ce cas, il sert de valeur de retour à la fonction.

Les ParamArray : l'attribut vararg

Pour déclarer que la liste des paramètres n'est pas connue à partir d'un certain paramètre, on utilise ParamArray suivi d'un nom de paramètre de type tableau de Variant. En langage ODL, il faut ajouter vararg dans les attributs de la méthode(et non dans les attributs du paramètre). Il s'en suit une définition de méthode comme suit :

[vararg] <type> <nom>(<liste de paramètres connus>, SAFEARRAY(VARIANT)* <nom arg>) ;

Les propriétés : les attributs propput, propputrefet propget

Pour déclarer une propriété en lecture-écriture, il faut deux fonctions : une pour lire et une pour écrire. Nous distinguerons deux cas :

  • Les propriétés normales,
  • Les propriétés objets.

Les propriétés normales : Property Get / Property Let (propget / propput)

Une propriété de ce type aura deux entrées dans la vtable. En langage ODL, elle auront le prototype suivant :

  • Pour la lecture, Property Get :
    • [propget] HRESULT <nom propriété>([out,retval] <type>* <nom>) ;
    • Function get_<nom propriété>(ByRef This, ByRef <nom> As <type>) As Long
  • Pour l'écriture, Property Let :
    • [propput] HRESULT <nom propriété>([in] <type> <nom>);
    • Function put_<nom propriété>(ByRef This, ByVal <nom> As <type>) As Long

Les propriétés objets : Property Get / Property Set (propget /propputref)

Une propriété de ce type aura deux entrées dans la vtable. En langage ODL, elle auront le prototype suivant :

  • Pour la lecture, Property Get :
    • [propget] HRESULT <nom propriété>([out,retval] <interface>** <nom>) ;
    • Function get_<nom propriété>(ByRef This, ByRef <nom> As <interface>) As Long
  • Pour l'écriture, Property Let :
    • [propputref] HRESULT <nom propriété>([in] <interface>* <nom>);
    • Function put_<nom propriété>(ByRef This, ByVal <nom> As <interface>) As Long

On peut aussi avoir des propriétés en lecture-seule ou écriture-seule en supprimant une des deux fonctions

Conclusion

Bon, et bien, vous allez surement encore me dire : "A part optimiser, qu'est ce que l'on peut faire de tout ça?". Et bien, utiliser des pointeurs de fonctions... mais ça c'est le tutorial suivant... Et puis l'implémentation de ForEach...

Ce document intitulé « Les objets légers : un concept pour optimiser les classes vb » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous