Ce paragraphe n'a pas la prétention de donner une description complète du langage IDL, ce qui dépasserait le cadre de cette FAQ.
Cependant, il explique un peu la syntaxe du langage et a pour but de permettre la rédaction de fichiers IDL de plus en plus complexes à l'aide de la référence du langage.
Le compilateur utilisé pour traiter les fichiers IDL est nommé MIDL.EXE (pour Microsoft IDL). MIDL est un outil complexe, qui permet de générer non seulement les stublets et les facelets, mais également les type libraries. Ce paragraphe ne présente que les fonctions élémentaires de MIDL, vous trouverez la référence complète de MIDL dans le SDK de Windows.
Le langage IDL est un langage qui ressemble au C++, en particulier pour la syntaxe de l'héritage. Il accepte également les directives du préprocesseur C, tout comme le C++. Les types de données sont exprimés avec une syntaxe équivalente à celle du C, et les types de base portent le même nom. En revanche, IDL possède un certain nombre de mots-clés qui permettent de décrire les interfaces, et il intègre également les mots clés du langage ODL afin de permettre la génération des type libraries.
|
En fait, MIDL utilise le préprocesseur C du compilateur Microsoft (CL.EXE). Il est donc nécessaire que CL.EXE ou qu'un programme de même nom qui l'émule soit accessible dans le path de votre ordinateur. Si ce n'est pas le cas, il est possible d'utiliser l'option /cpp_cmd pour indiquer le nom du préprocesseur que MIDL doit utiliser.
|
Le type int n'est pas accepté tel quel par MIDL. En effet, ce n'est pas un type portable, et IDL est supposé décrire les interfaces de manière indépendante de la plate-forme. Il faut donc toujours utiliser une version soit courte, soit longue de int dans les fichiers IDL.
Les types de base utilisés par IDL sont décrits ci-dessous :
- boolean, type de base pour les nombres booléens. La représentation est faite sur 8 bits, de manière non signée. Les deux seules valeurs possibles sont TRUE et FALSE. Ce type correspond au type bool du C++
- byte, type générique stocké sur 8 bits. Il n'a pas de notion de valeur ou de signe associée, les données de ce type sont transmises telles quelles sur le réseau. L'interprétation de ces données doit être faite au niveau des bits, les nombres stockées dans ce type peuvent changer de valeur selon la représentation interne de la machine cible. Ce type de donnée n'a pas d'équivalent direct en C/C++
- char, type de base pour les caractères, stocké sur 8 bits. Les caractères sont toujours considérés comme non signés par IDL. Ce type de donnée est équivalent au type unsigned char du C/C++
- wchar_t, type de base pour les caractères longs, stocké sur 16 bits. Les caractères sont toujours considérés comme non signés par IDL. Ce type de donnée est équivalent au type unsigned wchar_t du C++
- short, small, type de donnée permettant de stocker des entiers courts (16 bits). Par défaut, les valeurs stockées sont signées. Il est possible d'utiliser le mot-clé unsigned pour les rendre non signées. Ce type n'a pas d'équivalent direct en C/C++ (la taille des types n'est pas normalisée)
- int, long, type de donnée permettant de stocker des entiers normaux (32 bits). Par défaut, le type int est considéré comme égal au type long sur les plateformes 32 bits, cependant, sa taille est variable (tout comme en C/C++). Il est donc fortement recommandé de ne jamais l'utiliser sans qualification de taille. Les valeurs stockées sont signées, mais il est possible de les rendres non signées à l'aide du mot-clé unsigned. Ce type n'a pas d'équivalent en C/C++
- hyper, type de données permettant de stocker un entier long (64 bits). Par défaut, les valeurs stockées sont signées, mais elles peuvent devenir non signées si l'on utilise le mot-clé unsigned. Ce type est équivalent au type __int64 des compilateurs C/C++ pour Windows
- float, type de données permettant de stocker un nombre à virgule flottante sur 32 bits. Ce type n'a pas d'équivalent direct en C/C++ (le format des nombres à virgule flottante n'est pas normalisé)
- double, type de données permettant de stocker un nombre à virgule flottante sur 64 bits. Ce type n'a pas d'équivalent direct en C/C++
- void *, type de donnée permettant de stocker un pointeur sur un contexte d'exécution 32 bits
- handle_t, type de donnée primitif permettant de stocker un handle
|
Les mots-clés short, small, long et hyper sont en réalité des qualificateurs de taille du type int. Ils peuvent cependant être utilisés directement, sans le mot-clé int.
|
Les mots-clés short et small sont synonymes.
Il est possible de définir des structures et des types complexes à l'aide du mot-clé typedef du C. Par exemple :
typedef struct
{
long int i;
float *pf;
} s;
|
|
La syntaxe du C++ pour la définition des classes et des structures n'est pas acceptée par MIDL. Il faut utiliser le mot-clé typedef.
|
Tous les types prédéfinis de Windows et toutes les interfaces de base de DCOM doivent être définies au début du fichier IDL. Ces déclarations sont données dans le fichier unknwn.idl, cependant, ce fichier ne doit pas être inclus à l'aide de la directive #include du préprocesseur. En effet, ceci aurait pour conséquence d'inclure les déclarations des prototypes de fonctions des interfaces de base de DCOM, ce qui forcerait la création de stublets et de facelets pour ces fonctions. Or ceux-ci sont fournis par DCOM lui-même, et le fichier IDL ne pourrait donc pas être compilé. Pour résoudre ce problème, le mot-clé import a été défini dans IDL. Ce mot-clé permet d'inclure les déclarations d'un fichier IDL, mais pas les définitions. Sa syntaxe est la suivante :
|
On veillera surtout à ne pas oublier le point-virgule à la fin de la directive import. La syntaxe n'est donc pas la même que celle de la directive #include.
|
En fait, le mot-clé import permet également de lancer une nouvelle instance du préprocesseur sur le fichier importé. Ceci signifie que les macros définies dans ce fichier sont traitées indépendamment de celles du fichier qui l'importe, et qu'elles n'y sont donc plus définies.
Les interfaces sont définies à l'aide du mot-clé interface. Ce mot-clé doit être immédiatement suivi du nom de l'interface, puis d'une interface dont cette dernière hérite (elle hérite au moins de IUnknown), et enfin de la déclaration des méthodes de l'interface. La syntaxe utilisée est exactement celle du C++ pour la définition des classes, à ceci près que le mot-clé class a été remplacé par le mot-clé interface et que toutes les méthodes sont bien évidemment publiques :
interface nom : base
{
} ;
|
|
IDL ne supporte pas l'héritage multiple.
|
Il faut obligatoirement spécifier une interface de base.
La description des méthodes est réalisée avec une syntaxe similaire de la déclaration des fonctions en C. En fait, la syntaxe est exactement la même, à ceci près que des informations additionnelles sont données pour renseigner MIDL sur la manière dont ces fonctions seront utilisées.
D'une manière générale, un certain nombre d'éléments de la description peuvent recevoir des attributs qui les définissent précisément. Ces attributs précèdent toujours l'élément qu'ils qualifient et sont donnés entre crochets, à l'aide de mots-clés spécialisés. Lorsque plusieurs attributs sont donnés, ils sont séparés par des virgules :
[
attribut, attribut, etc...
]
|
L'un des attributs les plus important est introduit par le mot-clé uuid. C'est tout simplement le GUID associé à un objet donné (interface, composant, bibliothèque). Cet attribut doit apparaître en premier, avant les autres. La syntaxe du mot-clé uuid est donnée ci-dessous :
où valeur est le GUID représenté en hexadécimal, sans les accolades.
Un autre attribut important est l'attribut qui signale qu'une interface est définie dans le cadre de DCOM.
MIDL est capable de générer des proxy et des stubs pour les RPC en plus des facelets et des stublets pour DCOM (les fichiers générés ne portent d'ailleurs pas le même nom). Afin de lui signaler quel type de fichiers sources il doit générer, il faut utiliser le mot-clé object. La syntaxe de ce mot-clé est élémentaire, puisqu'il suffit de l'utiliser comme un simple attribut :
Viennent ensuite les attributs qui permettent de spécifier des informations pour l'utilisateur du composant. Ces informations apparaîtront dans les type libraries qui seront générées par MIDL. Le premier de ces attributs est la chaîne de caractères qui donne de l'aide sur un élément particulier. Cette chaîne de caractères est introduite à l'aide du mot-clé helpstring :
helpstring (" Description de l'élément qualifié par helpstring " )
|
Comme les type libraries peuvent contenir des messages textuels, elles sont dépendantes de la langue utilisée.
C'est pour cette raison que le mot-clé lcid a été introduit : il permet de spécifier la langue utilisée pour une
type library. Différents codes sont définis dans le fichier d'en-tête olenls.h.
Cependant, le code le plus courant est bien entendu 0x0000, qui ne spécifie aucune langue particulière.
La langue utilisée est alors un anglais neutre en général. La syntaxe du mot-clé lcid est donnée ci-dessous :
où code est le code du langage utilisé.
Enfin, le mot-clé version permet de spécifier la version d'une type library. Sa syntaxe est la suivante :
où numéro est le numéro de version. Ce numéro doit être constitué de deux nombres séparés par un point, par exemple 1.0.
Les attributs présentés ci-dessus servent uniquement à titre de renseignement, cependant, la plupart des attributs restants sont utilisés pour préciser la sémantique des méthodes des interfaces et de leurs paramètres. Ils permettent de renseigner MIDL sur la manière dont le code de marshalling doit être généré dans les facelets et les stublets. En particulier, les indications données suffisent à déterminer quels sont les paramètres en entrée, les paramètres en sortie d'une méthode, et la description des types complexes (tableaux, listes chaînées, etc...).
Les attributs in et out permettent de spécifier le sens dans lequel les paramètres sont passés entre l'appelant et l'appelé. Il est possible qu'un paramètre soit spécifié à la fois en tant que paramètre in et out. Il est très important de bien donner ces informations, car elles permettent à MIDL de ne copier les valeurs de ces paramètres que lorsque cela est nécessaire. Un paramètre in ne voit sa valeur copiée que de l'espace d'adressage du client vers celui du serveur avant l'appel de la fonction du serveur. Inversement, un paramètre out voit sa valeur copiée de l'espace d'adressage du serveur vers celui du client en retour de fonction. Enfin, les paramètres in, out sont copiés à la fois à l'appel et au retour de fonction. Ces copies pouvant se faire à travers un réseau, il est nécessaire de ne copier que les paramètres pour lequel ceci est vraiment nécessaire.
L'attribut async permet d'indiquer à MIDL qu'une méthode d'une interface doit être exécutée de manière asynchrone. Ceci signifie que le client poursuit son exécution juste après avoir effectué l'appel à cette méthode, sans attendre que celle-ci ne se termine. Bien entendu, dans ce cas, la méthode ne peut pas renvoyer de code d'erreur, le client ne l'utiliserait d'ailleurs pas. Elle doit donc renvoyer void. De même, tous ses paramètres doivent être in seulement, aucun paramètre ne peut être modifié en retour par le serveur.
L'attribut string permet d'indiquer à MIDL que le paramètre qualifié est une chaîne de caractère. Le paramètre en question doit être un tableau d'éléments du type char, wchar_t, byte ou un type équivalent, ou un pointeur sur un tableau d'éléments de ce type. Lorsque cet attribut est utilisé, la longueur de la chaîne de caractère est déterminée dynamiquement avec la convention du C : la fin de chaîne est marquée par un caractère nul. Cet attribut ne peut donc pas être utilisé avec des langages qui ne traitent pas les chaînes de caractères comme le C. Si l'on veut transmettre une chaîne de caractères de taille fixe, il est préférable de décrire cette chaîne de caractères comme un tableau.
Les tableaux sont déclarés avec la syntaxe du C/C++ dans MIDL. MIDL utilise la même sémantique que le C/C++ pour les tableaux : la borne inférieure doit toujours être 0, et les tableaux de dimension deux ou plus sont traités comme des tableaux de tableaux, et il n'est pas obligatoire de préciser la taille de la dernière dimension. Cependant, dans ce cas, la borne supérieure doit pouvoir être déterminée lors de l'exécution du programme. Pour cela, il faut indiquer le nom d'un identificateur qui contiendra la taille du tableau. Lorsque le tableau est défini à l'intérieur d'une structure, cet identificateur doit être l'un des champs de cette structure, et lorsque le tableau est un des paramètres d'une fonction, cet identificateur doit être un autre paramètre de cette fonction. Pour spécifier quel identificateur doit être utilisé, il faut utiliser le mot-clé size_is. Sa syntaxe est la suivante :
[size_is (identificateur)] tableau[]
|
Lorsque l'on utilise des tableaux de pointeurs, il est nécessaire que chacun des éléments du tableau soit initialisé et pointent sur de la mémoire valide.
La manipulation des pointeurs est une opération difficile à réaliser pour MIDL, pour diverses raisons. La première est que lorsqu'un pointeur est passé en paramètre, il est nécessaire d'effectuer les opérations de marshalling pour les données pointées en plus du pointeur. La deuxième raison est que les pointeurs et les références conduisent souvent à la création d'alias dans le code, c'est à dire à la possibilité d'accéder de plusieurs manières différentes à une même variable donnée (par exemple, l'identificateur qui la représente et par son adresse). Afin de faciliter la tâche à MIDL, un certain nombre d'attributs sont disponibles pour qualifier les pointeurs et préciser leurs propriétés.
Si le pointeur utilisé ne dispose d'aucune propriété particulière, MIDL doit effectuer toutes les vérifications avant d'effectuer le marshalling de ce pointeur. En particulier, il doit vérifier la présence d'alias et de cycles de pointeurs éventuels dans les structures chaînées par pointeurs. Ce type de pointeurs qui demandent le plus de calculs, peut être déclaré à l'aide du qualificateur ptr. Les propriétés de ces pointeurs sont les suivantes :
- ils peuvent avoir la valeur nulle à l'appel
- le serveur peut changer leur valeur
- les données pointées peuvent être accédées par d'autres identificateurs accessibles par le serveur
Si le pointeur utilisé est le seul moyen dont dispose le serveur pour accéder aux données qu'il référence, ou autrement dit s'il n'y a pas d'alias sur ces données, ce pointeur est dit unique. Les pointeurs uniques peuvent être qualifiés à l'aide du mot-clé unique. Les pointeurs uniques possèdent les propriétés suivantes :
- ils peuvent avoir la valeur nulle à l'appel
- le serveur peut changer leur valeur
Enfin, si le pointeur n'est utilisé que pour accéder à une donnée par indirection, c'est à dire si la donnée peut être passée directement par valeur lors de l'appel, le pointeur est dit pointeur par référence. Les pointeurs de ce type sont qualifiés par le mot-clé ref. Les pointeurs par références doivent respecter les contraintes suivantes :
- ils doivent être initialisés lors de l'appel, et pointer sur des données valides (autrement dit, ils ne peuvent pas contenir la valeur nulle)
- ils ne doivent pas changer de valeur au cours de l'exécution du code du serveur
- ils ne doivent pas être des alias d'autres identificateurs
Il est évident que les pointeurs par références sont les plus efficaces. En pratique, on utilisera souvent les pointeurs uniques cependant, car ils conviennent dans la plupart des situations.
Lors de la manipulation des pointeurs, un certain nombre de problèmes peuvent se poser, essentiellement lorsqu'une allocation mémoire doit avoir lieu. Une des règles à respecter est que pour les pointeurs qui ne sont pas par références, les données pointées doivent obligatoirement être allouées, parce que le serveur est susceptible de désallouer cette mémoire. De plus, les allocations et libérations de mémoire doivent toujours se faire avec l'allocateur de DCOM, par l'intermédiaire de l'interface IMalloc. Enfin, il faut savoir que de la mémoire peut être perdue lorsqu'un pointeur passé en paramètre contient une adresse valide sur un bloc alloué et que le serveur met à la valeur nulle ce pointeur. Si l'adresse n'est pas stockée au niveau du client dans une variable annexe, le bloc de mémoire est perdu. Il est également bon de savoir que lorsque le serveur change la valeur d'un pointeur en une autre valeur, les données pointées par la nouvelle valeur sont supposée être du même type que celles pointées par l'ancienne valeur. Ceci implique qu'elles ont la même taille, et que dans le cas des pointeurs out, les données sont recopiées au retour de la fonction dans le même bloc mémoire que celui qui a été utilisé par le client pour les passer. Le pointeur ne change donc pas de valeur du côté du client.
Nous avons vu la manière de décrire les interface, les méthodes et les paramètres des méthodes dans les paragraphe suivant. Mais MIDL permet de faire bien plus que cela, puisque depuis la version 3 de MIDL, il est possible d'inclure la définition des type libraries dans les fichiers IDL. Ceci permet d'éviter d'avoir à redéfinir les interfaces à la fois dans les fichiers IDL et dans les fichiers ODL : une seule définition suffit à présent. Nous allons maintenant voir comment les type libraries sont définies dans les fichiers IDL.
La génération d'une type library se fait à l'aide du mot-clé library. Ce mot-clé s'utilise avec la syntaxe suivante :
library nom_librairie
{
coclass nom_composant
{
interface I1;
interface I2;
etc...
}
} ;
|
où nom_librairie est le nom de la type library que MIDL va générer (Attention ! Ce n'est pas forcément le nom du fichier .TLB généré), nom_composant est le nom d'un des composants décrits par cette bibliothèque et I1, I2, etc sont des interfaces déjà définies.
|
La syntaxe donnée ci-dessus est simplifiée. C'est la syntaxe minimale pour générer une type library.
|
Les attributs des différents éléments n'ont pas été précisés. En réalité, il faut donner au moins un GUID pour la type library et un CLSID pour le composant.
Pour illustrer ce qui a été dit jusqu'à présent, un fichier IDL simple est fournit ci-dessous. Cet exemple complet est le fichier IDL utilisé pour la description du composant Adder. Ce fichier est commenté à chaque étape de la description et sa lecture ne devrait pas poser de problèmes.
Exemple 12. Fichier IDL pour le composant Adder
import " unknwn.idl " ;
[
uuid (e3261620-0ded-11d2-86cc-444553540000),
object,
helpstring (" Definition of the IAdder interface " )
]
interface IAdder : IUnknown
{
[
helpstring (" Adds two integers and returns the result " )
]
HRESULT Add ([in ] long i, [in ] long j, [out ] long *pResult);
[
helpstring (" Substracts two integers and returns the result " )
]
HRESULT Sub ([in ] long i, [in ] long j, [out ] long *pResult);
} ;
[
uuid (e3261621-0ded-11d2-86cc-444553540000), object,
helpstring (" Definition of the IOpposite interface " )
]
interface IOpposite : IUnknown
{
[
helpstring (" Calculates the opposite of an integer " )
]
HRESULT Opposite ([in ] long i, [out ] long *pResult);
} ;
[
uuid (128abb80-0e9a-11d2-86cc-444553540000),
helpstring (" Adder Type Library " ),
lcid (0x0000),
version (1.0)
]
library AdderTypeLibrary
{
[
uuid (91e132a0-0df1-11d2-86cc-444553540000),
helpstring (" Description of the Adder Component " )
]
coclass Adder
{
interface IAdder;
interface IOpposite;
}
} ;
|
Voici à présent le fichier IDL contenant la description de l'interface du composant Calculator :
Exemple 13. Fichier IDL pour le composant Calculator
import " unknwn.idl " ;
[
uuid (e3261622-0ded-11d2-86cc-444553540000), object,
helpstring (" Definition of the IMultiplier interface " )
]
interface IMultiplier : IUnknown
{
[
helpstring (" Multiplies two integers and returns the result " )
]
HRESULT Mul ([in ] long i, [in ] long j, [out ] long *pResult);
} ;
import " ..\..\Adder\AdderPrx\adder.idl " ;
[
uuid (128abb81-0e9a-11d2-86cc-444553540000),
helpstring (" Calculator Type Library " ),
lcid (0x0000),
version (1.0)
]
library CalculatorTypeLibrary
{
[
uuid (91e132a1-0df1-11d2-86cc-444553540000),
helpstring (" Description of the Calculator Component " )
]
coclass Calculator
{
interface IAdder;
interface IOpposite;
interface IMultiplier;
}
} ;
|
|