I. Avant-Propos

Ecrire sa propre dll en C, pour l'appeler depuis VB, peut être utile dans certains cas :

  • si vous avez besoin d'une fonctionnalité difficile ou impossible à réaliser en VB
  • si vous devez utiliser des librairies qui n'ont pas été conçues pour être facilement utilisées en VB
  • si une partie de votre programme a besoin d'une grande vitesse d'exécution

Dans cet article nous supposerons que vous savez créer une dll avec votre compilateur C, et nous n'aborderons donc que la compatibilité et l'utilisation d'une dll avec VB 6.

Les codes sources ont été testés avec Visual Basic 6 et Visual C++ 6.

II. Convention d'appel des arguments

Le passage d'arguments à une fonction correspond pour la machine à une copie de ces arguments dans une zone mémoire que l'on appelle la pile. Selon la convention utilisée, ces arguments seront copiés de gauche à droite, c'est-à-dire dans l'ordre où ils sont écrits dans votre source ou de droite à gauche. Visual Basic 6, comme beaucoup d'autres langages (Pascal, Fortran, etc...), passe les arguments de droite à gauche. Vous n'avez pas à vous en soucier si votre programme est basé sur un seul langage, mais ici votre dll en C doit utiliser la même convention que Visual Basic. Pour cette raison vous devez utiliser le mot clé __stdcall.

III. 2. Exemple de fonction simple

Le fichier en-tête Def.h:
Sélectionnez
#include "windows.h"
#define export __declspec (dllexport)

export void __stdcall HelloWorld(void ); 
Fichier .c :
Sélectionnez
#include "Def.h"

void __stdcall HelloWorld(void )
{
    MessageBox(NULL,"Hello !","Message",MB_OK);
} 

IV. Noms des fonctions exportées

Chez certains compilateurs, dont Visual C++ 6 et MinGW, l'utilisation de __stdcall a pour effet de modifier les noms des fonctions exportées lors de la compilation (cette modification s'appelle une décoration). Par exemple, avec Visual C++ 6 notre fonction HelloWorld() sera exportée sous le nom " _HelloWorld@0 ". Mais vous pouvez tout de même spécifier le nom qu'aura votre fonction en l'indiquant dans un fichier .def. Voici un exemple de fichier .def :

 
Sélectionnez
LIBRARY Fonctions
DESCRIPTION "Essai de dll"
EXPORTS
HelloWorld _HelloWorld@0

La première ligne contient le nom de la dll. La deuxième ligne, optionnelle, est une description de la dll. Les noms des fonctions exportées commencent après le mot clé EXPORTS. Pour chaque fonction, le nom que vous voulez lui donner est suivi du nom qui a été donné par le compilateur.

Note pour Visual C++ 6 : pour connaître les noms des fonctions donnés par le compilateur, cochez l'option "Generate Map File" dans l'onglet "Link" de la fenêtre des propriétés du projet. Ainsi un fichier .map contenant ces informations sera généré lors de la compilation.

V. Appel d'une fonction depuis Visual Basic

Nous déclarons notre fonction, qui pour VB est en fait ici une procédure, en suivant la syntaxe habituelle :

 
Sélectionnez
Private Declare Sub HelloWorld Lib "Fonctions.dll" () 

Le nom de la fonction est sensible à la casse : si vous déclarez la fonction "helloworld" alors que la dll contient la fonction "HelloWorld", vous aurez un message d'erreur.

A l'exécution du programme, Windows recherche la dll dans le répertoire courant, dans celui de l'exécutable ainsi que dans les répertoires Windows et Windows\System. Lorsque vous exécutez votre projet dans Visual Basic, ce n'est pas l'exécutable qui est dans le répertoire de votre projet qui est lancé. Donc si votre dll n'est que dans le répertoire de votre projet, vous risquez d'obtenir un message d'erreur vous signifiant que le fichier de la dll est introuvable. Pour éviter ce problème, copiez votre dll dans le répertoire Windows ou Windows\System.

Si vous recevez le message d'erreur "Can't find DLL entry point ...", c'est parce que la fonction que vous essayez d'appeler n'a pas été exportée, ou a été exportée sous un nom différent.

VI. Correspondance des types

Pour pouvoir écrire en VB les déclarations des fonctions et des structures de la dll, vous devez connaître les équivalences entre les types de variables du C et ceux de Visual Basic.

VB 6 C / C++
Integer bool, short
Long int, long
N/A unsigned short, unsigned int, unsigned long
Single float
Double double

En C un int occupe 2 octets sur un système 16 Bits (Windows 3.1 par exemple) et 4 octets sur un système 32 bits.

Il n'y a pas d'équivalent en VB aux types non signés du C. Dans vos déclarations vous pourrez faire correspondre un "unsigned short" à un Integer en VB, et un "unsigned long" ou un "unsigned int" à un Long. Mais n'oubliez pas que les valeurs maximales autorisées pour les variables de types non signés sont le double de celles pour les types signés.

La constante true vaut -1 en vb, alors qu'elle est égale à 1 en C++.

VII. Passer une chaîne de caractères par valeur

En VB les chaînes sont stockées sous la forme d'un type appelé BSTR. En C, le type de chaîne est LPSTR qui est un pointeur vers une chaîne terminée par le caractère nul. Passer une chaîne BSTR par valeur revient à transmettre un pointeur sur le premier caractère de la chaîne, ce qu'attend votre fonction C.

Avant de passer la chaîne à la fonction C, vous devez lui allouer un espace mémoire suffisant, par exemple avec la fonction String().

Dans cet exemple la chaîne passée en paramètre est modifiée par la fonction GetMessage.

La fonction C :
Sélectionnez


void __stdcall GetMessage(LPSTR chaine)
{
strcpy(chaine,"Hello World !");
} 
L'appel en vb :
Sélectionnez
Private Declare Sub GetMessage Lib "Fonctions.dll" (ByVal chaine As String )

Sub Test()

Dim chaine As String 
chaine = String(255, vbNullChar)
GetMessage chaine

End Sub 

VIII. Passer une chaîne de caractères par référence

Si la fonction C attend un pointeur sur un LPSTR, passez la chaîne de caractères par référence.

 
Sélectionnez
void __stdcall GetMessage(LPSTR *chaine)
{
strcpy(*chaine,"Hello World !");
}
 
Sélectionnez
Private Declare Sub GetMessage Lib "Fonctions.dll" (ByRef chaine As String )

Sub Test()

Dim chaine As String 
chaine = String (255, vbNullChar)
GetMessage chaine

End Sub 

IX. Chaîne de longueur fixe

En VB :
Sélectionnez
Dim chaine as String * 10 
En C :
Sélectionnez
char chaine[10]; 

La ligne écrite en VB déclare une chaîne pouvant contenir 10 caractères. La ligne écrite en C déclare une chaîne pouvant contenir 9 caractères, le 10 ème étant réservé pour le caractère nul marquant la fin de la chaîne. Indiquez dans le programme VB et dans la dll la même taille pour vos chaînes de caractères fixes, comme dans l'exemple ci-dessus, mais sachez que le nombre de caractères disponibles sera la taille indiquée - 1 (donc ici 9 caractères).

X. Les types définis par l'utilisateur (UDT)

X-A. Alignement des champs

VB 6 aligne les champs d'un UDT sur 4 octets. Pour pouvoir passer un UDT à votre dll, celle-ci doit stocker les structures de la même façon. Les champs de type short, byte ou bool de vos structures C doivent donc occuper 4 octets.

Note pour Visual C++ 6 : Pour pouvoir passer correctement des UDT à votre dll, allez dans les paramètres du projet et dans l'onglet C/C++, sélectionnez la catégorie "Code Generation". Donnez au champ "Struct member alignement" la valeur "4 Bytes ". Ceci ajoute "/Zp4" dans les options du projet.

Les UDT doivent être passés par référence. Les noms des champs ne sont pas obligatoirement les mêmes, seul le type de chaque champ doit correspondre.

X-B. Chaînes de caractères dans un UDT

Les chaînes de longueur fixe sont contenues tel quel dans la structure, aussi bien en VB qu'en C. Celles de longueur variable sont stockées en C sous la forme d'un pointeur vers le premier caractère, donc elles peuvent être déclarées de type String en VB. Dans l'exemple ci-dessous, les champs d'une structure sont modifiés par la dll :

 
Sélectionnez
struct DATA {
    short x;
    long y;
    char strFixe[11];
    double reel;
    LPSTR strVariable;
} ;

void __stdcall InitData (DATA * data)
{
data->x = 15;
data->y = 52445;
lstrcpyn(data->strFixe,"abcdefghij",11);
data->reel = 2459.65;
strcpy(data->strVariable,"chaîne de longueur variable");
} 
 
Sélectionnez
Private Type TDATA
    x As Integer 
    y As Long 
    strFixe As String * 11
    reel As Double 
    strVariable As String 
End Type

Private Declare Sub InitData Lib "Fonctions.dll" (donnees As TDATA)

Sub Test()

Dim donnees As TDATA

donnees.strVariable = Space(30)
InitData donnees

End Sub 

XI. Passer un tableau de numériques

Passez simplement le premier élément du tableau par référence. Ainsi la fonction C recevra l'adresse du premier élément. Dans cet exemple, la fonction DoubleElements() multiplie par 2 tous les éléments d'un tableau, le nombre d'éléments étant passé dans le deuxième paramètre.

 
Sélectionnez
void __stdcall DoubleElements(int * tableau, long lngNbItems)
{
long i;
for (i=0;i<;lngNbItems;i++) {
    tableau[i] = 2 * tableau[i];
} 
} 
 
Sélectionnez
Private Declare Sub DoubleElements Lib "Fonctions.dll" (tableau As Long , ByVal lngNbItems As Long )

Sub Test()

Dim elements(0 To 9) As Long , i As Long 

For i = 0 To 9
    elements(i) = i
Next 
DoubleElements elements(0), 10

End Sub 

XII. Passer un tableau de chaînes de caractères

En VB les tableaux de chaînes de caractères et les tableaux de structures sont stockés sous la forme d'un descripteur de tableau appelé SAFEARRAY, dont voici la déclaration en C :

 
Sélectionnez
typedef struct FARSTRUCT tagSAFEARRAY
{
    unsigned short cDims; // nombre de dimensions
    unsigned short fFeatures; // flags indiquant les caractéristiques du tableau
    unsigned long cbElements; // taille d'un élément dans le tableau
    unsigned long cLocks; // nombre de verrous posés sur le tableau
    void HUGEP* pvData; // pointeur sur les données
    SAFEARRAYBOUND rgsabound[1]; // limites du tableau     
} SAFEARRAY; 

La structure SAFEARRAYBOUND étant déclarée ainsi :

 
Sélectionnez
typedef struct tagSAFEARRAYBOUND
{
    unsigned long cElements; //Nombres d'éléments
    long LBound; //Limite inférieure     
} SAFEARRAYBOUND; 

Une fonction C qui reçoit en paramètre un tableau Visual Basic de type String doit déclarer ce tableau sous la forme d'un pointeur vers un pointeur sur un SAFEARRAY. Les chaînes de caractères contenues dans ce tableau sont de type BSTR.

Vous pouvez accéder aux données du tableau en appelant la fonction SafeArrayAccessData. Elle reçoit en paramètres un pointeur vers le descripteur de tableau et l'adresse de la destination. Après l'exécution de la fonction, le deuxième paramètre contient un pointeur sur un pointeur vers les données, et le nombre de verrous du descripteur de tableau est incrémenté. Quand vous n'avez plus besoin du tableau vous devez appeler la fonction SafeArrayUnaccessData.

Dans l'exemple suivant, la fonction UpperCaseElements met en majuscules toutes les chaînes de caractères d'un tableau.

 
Sélectionnez
void __stdcall UpperCaseElements(SAFEARRAY **tableau)
{
BSTR *chaine;
HRESULT ret;
unsigned long i;

if ((ret = SafeArrayAccessData(*tableau,(void **) &chaine))==S_OK)
{
    for (i = 0; i < (*tableau)->rgsabound->cElements; i++) {
        CharUpper((LPTSTR) chaine[i]);
    } 
    SafeArrayUnaccessData(*tableau);
} 
} 
 
Sélectionnez
Private Declare Sub UpperCaseElements Lib "Fonctions.dll" (tableau() As String )

Sub Test()

Dim elements(0 To 5) As String , i As Long 

For i = 0 To 5
    elements(i) = "élément " & i
Next 

UpperCaseElements elements()

End Sub 

XIII. Passer un tableau de structures

Nous utilisons là aussi les SAFEARRAY. Le principe est le même que pour les tableaux de chaînes de caractères.

 
Sélectionnez
struct DATA {
    long x;
    BSTR chaine;
} 

void __stdcall InitArray(SAFEARRAY **tableau)
{
DATA *elt;
HRESULT ret;
unsigned long i;

if ((ret = SafeArrayAccessData(*tableau,(void **) &elt))==S_OK)
{
    for (i = 0; i < (*tableau)->rgsabound->cElements; i++) {
        elt[i].x ++;
        SysFreeString(elt[i].chaine);
        elt[i].chaine = SysAllocString(OLESTR("hello"));
    } 
    SafeArrayUnaccessData(*tableau);
} 
} 
 
Sélectionnez
Private Type data
    x As Long 
    chaine As String 
End Type

Private Declare Sub InitArray Lib "Dvp.dll" (tableau() As data)

Sub Test()

Dim elements(2 To 6) As data, i As Long 

For i = 2 To 6
    elements(i).x = 10
    elements(i).chaine = "chaine " & i
Next 

InitArray elements()

End Sub 

XIV. Articles sur Visual Basic 6