I. Avant-propos▲
Écrire 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▲
#include "windows.h"
#define export __declspec (dllexport)
export void
__stdcall HelloWorld
(
void
);
#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 :
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 :
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.
À 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 chaine de caractères par valeur▲
En VB les chaines sont stockées sous la forme d'un type appelé BSTR. En C, le type de chaine est LPSTR qui est un pointeur vers une chaine terminée par le caractère nul. Passer une chaine BSTR par valeur revient à transmettre un pointeur sur le premier caractère de la chaine, ce qu'attend votre fonction C.
Avant de passer la chaine à la fonction C, vous devez lui allouer un espace mémoire suffisant, par exemple avec la fonction String().
Dans cet exemple la chaine passée en paramètre est modifiée par la fonction GetMessage.
void
__stdcall GetMessage
(
LPSTR chaine)
{
strcpy
(
chaine,"
Hello World !
"
);
}
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 chaine de caractères par référence▲
Si la fonction C attend un pointeur sur un LPSTR, passez la chaine de caractères par référence.
void
__stdcall GetMessage
(
LPSTR *
chaine)
{
strcpy
(*
chaine,"
Hello World !
"
);
}
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. Chaine de longueur fixe▲
Dim
chaine as
String
*
10
char
chaine[10
];
La ligne écrite en VB déclare une chaine pouvant contenir 10 caractères. La ligne écrite en C déclare une chaine pouvant contenir 9 caractères, le 10e étant réservé pour le caractère nul marquant la fin de la chaine. Indiquez dans le programme VB et dans la dll la même taille pour vos chaines 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. Chaines de caractères dans un UDT▲
Les chaines 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 :
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,"
chaine de longueur variable
"
);
}
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.
void
__stdcall DoubleElements
(
int
*
tableau, long
lngNbItems)
{
long
i;
for
(
i=
0
;i<
;lngNbItems;i++
) {
tableau[i] =
2
*
tableau[i];
}
}
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 chaines de caractères▲
En VB les tableaux de chaines 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 :
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 :
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 chaines 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 chaines de caractères d'un tableau.
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);
}
}
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 chaines de caractères.
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);
}
}
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