AdpService for .NET
Téléchargements en rapport
Depuis la première annonce de la plate-forme Atom Developer, la création d'applications dans .NET a suscité un intérêt important. Début décembre, yllian-saint-hilare a posté sur le blog un article consacré à Building .NET applications for the store. Ce post expliquait la procédure à suivre afin de créer pour le SDK ADP un encapsuleur .NET autour d'une DLL C++ non managée. Il en résultait la création de deux DLL à inclure dans votre application .NET. Le présent article part du travail réalisé par Yllian tout en restructurant le code pour lui faire produire une seule DLL managée encapsulant la totalité du SDK ADP.
Remarque : cette discussion et le code qui l'accompagne sont destinés au SDK ADP d'Intel version 0.92.
J'ai juste besoin du code
Si vous cherchez seulement à télécharger la DLL pour créer ou porter votre application sur ADP, utilisez le lien à droite. Il vous mène à un fichier zip contenant la DLL binaire que vous pouvez faire glisser sur votre application.
1. Créez une nouvelle application Windows Forms.

2. Copiez la DLL dans le dossier solution de votre application (ou, mieux, dans un sous-dossier lib du dossier solution).
3. Dans Visual Studio, ajoutez une référence en naviguant vers le fichier de la DLL.

4. Instanciez un objet com.intel.adp.AdpWrapper dans le code de démarrage de votre application et appelez, au minimum, Initialize et IsAuthorized.
Pour plus de détails sur la création d'une application à l'aide de ce code, reportez-vous à l'article Facebook Example in .NET.
Création de la DLL .NET pour ADP
La discussion qui suit s'appuie sur le travail d'Yllian, sans lequel rien de ceci n'aurait été possible. J'espère que je serai assez clair si vous souhaitez créer et configurer les choses vous-même, améliorer la DLL ou la mettre à jour pour de nouvelles versions du SDK ADP.
Pour plus de clarté, je me contenterai d'extraire des parties du code. De votre côté, vous pouvez toujours télécharger la totalité de la solution à partir du lien à droite et examiner le code.
Les bibliothèques du SDK ADP sont livrées sous forme de fichiers .lib, qui sont destinés à être liés de manière statique à votre projet. Pour que .NET puisse accéder au code non managé, il doit être capable de référencer une DLL. Vous pouvez faire un bind statique du code non managé vers une DLL managée (c'est-à-dire .NET), mais uniquement si vous programmez en C++ managé.
Création d'une DLL managée pour référencer la bibliothèque ADP
En créant ma DLL à partir de rien, j'ai ouvert Visual Studio 2008 et j'ai créé un nouveau projet de bibliothèque de classes C++ CLR, que j'ai appelé AdpService. Remarquez au passage qu'il s'agit de CLR (Common Language Runtime), qui permet à la DLL d'être utilisée par les applications .NET.

Comme je vais exposer une méthode managée pour chacune des fonctions du SDK ADP, depuis le dossier du SDK, j'ai copié adpcode.lib et adpcode.h vers le dossier de ce projet afin que le linker puisse la trouver. J'ai utilisé l'API C car elle est plus simple à utiliser que celle de C++, mais j'exposerai les appels au SDK via une classe managée.
Pour être sûr que les références au projet sont bien là, j'ai ajouté adpcore.h en tant qu'include dans le fichier AppService.h et comme élément existant aux fichiers header du projet.
Enfin, nous devons indiquer au linker de rechercher la bibliothèque. Dans les propriétés du projet, l'option All configurations doit être sélectionnée dans le coin supérieur droit. Sélectionnez ensuite Linker/Input et ajoutez adpcore.lib psapi.lib shlwapi.lib Advapi32.lib (vous avez besoin des quatre) à l'entrée Additional Dependencies.

Création d'une classe .NET permettant d'accéder au système ADP
Pour traduire à partir de .NET en code non managé (adpcore.lib), nous allons devoir dupliquer chaque fonction, chaque typedef et chaque enum et convertir les paramètres des données entre le type managé et le type non managé1.
J'ai créé dans le fichier header une classe enum publique qui réplique l'enum située dans le fichier header C adpcore.h. La conversion des paramètres s'effectuera directement car enum représente un entier signé. Chaque appel à des fonctions de la bibliothèque C retournera une valeur que nous pouvons transtyper en type enum.
Les fonctions de la bibliothèque C pourraient être des méthodes statiques dans la classe .NET ; mais comme, pour tester notre code avec l'intégration d'ADP, nous allons vouloir simuler le service ADP, nous voudrons en faire une classe avec une méthode d'instance et une interface sous-jacente.
Un examen d'AdpService.h dans le source vous montrera que tous les appels de méthodes ne sont pas statiques, pas plus que les deux propriétés non constantes.
public ref class AdpService : IAdpService
{
public:
virtual ReturnCode Initialize();
virtual ReturnCode Close();
virtual ReturnCode ReportCrash( ... );
virtual ReturnCode IsAuthorized(Guid applicationid);
virtual ReturnCode IsAppAuthorized(Guid componentid);
virtual ReturnCode ApplicationBeginEvent();
virtual ReturnCode ApplicationEndEvent();
virtual property String^ ApiVersion{ String^ get(); };
virtual property unsigned long ApiLevel{ unsigned long get(); };
property static Guid DebugApplicationId
{
...
}
property static Guid DebugComponentId
{
....
}
};
Si vous êtes habitué à l'API C++ du SDK, vous remarquerez que vous pouvez soit hériter de cette classe, soit en utiliser un objet. À la différence du SDK, cela ne fournit pas immédiatement l'initialisation et l'autorisation via un constructeur. Vous pouvez tout à fait l'ajouter vous-même si vous le voulez, mais je préconise fortement de faire de l'intégration d'ADP un objet composite (ou delegate) vers un objet AdpService à partir de la classe principale de votre programme, et ce, pour trois raisons :
- 1. Votre application n'est pas un service d'autorisation ; aussi ne doit-elle pas faire de ce dernier une sous-classe purement basée sur le contexte du domaine.
- 2. L'utilisation de sous-classes peut provoquer la levée d'une exception par le constructeur parent, exception qui ne sera pas correctement gérée (vous obtenez un objet initié de manière incorrecte). Un post sur le forum aborde tout cela en détail.
- 3. La création de composite vous permet d'utiliser l'injection de dépendances et de remplacer cela dans votre application par un objet factice pendant les tests, ce qui vous permet d'automatiser vos tests sans exécuter ATDS.
Conversion des GUID
Le SDK utilise des GUID pour les ID d'applications et de composants. Comme il s'agit de GUID standard et que .NET possède un type intégré System.Guid, il m'a paru judicieux d'utiliser le type intégré 2. Les deux méthodes d'autorisation acceptent les types Guid dans leur signature et j'ai créé deux propriétés qui retournent les types Guid pour l'ID de débogage utilisé pour le développement et les tests des applications et des composants.
property static Guid DebugApplicationId
{
Guid get()
{
array<unsigned char>^ id = gcnew array<unsigned char>(16);
pin_ptr<unsigned char> dst = &id[0];
memcpy(dst, &ADP_DEBUG_APPLICATIONID, 16);
return Guid(id);
}
}
Le code ci-dessus nous permet d'obtenir du code non managé dans de la mémoire managée et de convertir ce code en type. Tout d'abord, j'ai utilisé System.Guid comme type de retour de valeur (et non comme référence « Guid^ ») car il s'agit d'un struct et non d'une classe.
Dans le getter, nous créons un nouveau tableau d'octets dans la mémoire managée (« gcnew » signifie qu'il est inclus dans la récupération de place). Petit rappel : dans le C++ managé de Microsoft, le ^ équivaut à un * (c'est-à-dire, qu'il signale une référence), mais il est utilisé pour les références managées.
Ensuite, nous positionnons un pointeur au début de notre tableau. Cela indique au marshaler que nous voulons utiliser la même mémoire dans l'appel non managé de manière à pouvoir récupérer les résultats quand nous aurons fini.
Puis nous utilisons la fonction C standard memcpy pour copier octet après octet vers notre tableau d'octets la valeur détenue au niveau du pointeur ADP_DEBUG_APPLICTIONID dans la bibliothèque C. À noter que j'ai dû ajouter cette fonction
Enfin, nous créons un type Guid qui passe le tableau d'octets au constructeur.
Il est important de noter ici que je sais que les données d'ADP_DEBUG_APPLICATIONID font 16 octets. Si vous créez ce code par rapport à une autre version de l'API C, vous devez le vérifier ou modifier le code en conséquence.
Conversion des paramètres des données
Les signatures de méthodes ci-dessus indiquent clairement qu'aucune donnée ne va dans la plupart des appels, ce qui nous facilite la tâche de traduction. Encore plus utile est le fait que toutes les données allant dans les appels sont constantes (c'est-à-dire que les fonctions ne modifient pas les données). Cela facilite la conversion des paramètres de données car nous n'avons pas à positionner un pointeur pour nos appels à l'API (nous pouvons laisser le convertisseur de paramètres effectuer des copies des données pour qu'elles soient utilisées dans le code non managé).
Dans l'ensemble du code source, vous pouvez constater que les appels simples à l'API, comme Initialize, se contentent d'appeler l'API C et de retourner un transtypage de valeur dans notre type ReturnCode :
ReturnCode AdpService::Initialize()
{
return (ReturnCode)ADP_Initialize();
}
Les méthodes d'autorisation prennent chacune un paramètre Guid et inversent ce que nous avons fait plus haut dans les propriétés, en convertissant le Guid en un tableau d'octets managé, en copiant ce tableau vers le type non managé approprié et en passant le résultat en tant que paramètre à l'appel à l'API C.
ReturnCode AdpService::IsAuthorized(Guid applicationId)
{
ADP_APPLICATIONID id;
array<unsigned char>^ guidBytes = applicationId.ToByteArray();
pin_ptr<unsigned char> src = &guidBytes[0];
memcpy(&id, src, 16);
return (ReturnCode)ADP_IsAuthorized(id);
}
Convertir des chaînes de managées à non managées est chose compliquée car, en C++ non managé, l'on peut se retrouver avec un grand nombre de types différents de chaînes. Les chaînes peuvent en effet être à un seul octet ou à double octet, terminées par null (cas habituel), muables (const) ou immuables, etc. Le C++ managé de Microsoft comporte des méthodes facilitant la conversion de paramètres des chaînes. En ajoutant msclr::interop aux références d'espaces de noms, nous pouvons utiliser la fonction marshal_as pour convertir des données de chaîne comme je l'ai fait dans la propriété ApiVersion :
String^ AdpService::ApiVersion::get()
{
return marshal_as<System::String^>(ADP_API_VERSION);
}
La méthode marshal_as peut être appelée dans un contexte statique (c'est-à-dire comme fonction) lors de la conversion de chaînes natives en chaînes managées comme nous l'avons fait plus haut. Lors de la conversion de paramètres vers des chaînes non managées, vous devez commencer par créer un contexte de conversion des paramètres au sein duquel convertir les paramètres de la chaîne3. La conversion de paramètres de données la plus difficile pour cette API a été la fonction de rapport de plangage en raison du nombre et de la complexité des paramètres.
ReturnCode AdpService::ReportCrash(
String^ module,
unsigned long lineNumber,
String^ message,
String^ category,
String^ errorData,
array<CrashReportField^>^ customFields )
L'API C prend ces paramètres plus deux autres : la longueur de la chaîne errorData et le nombre d'éléments dans le tableau customFields. Comme il a été possible de déterminer ces deux paramètres dans le contexte .NET, je les ai évalués au sein de l'appel plutôt que de forcer le développeur à les inclure.
Pour pouvoir convertir les paramètres des données de références String managées à des « const wchar_t* » non managés, j'ai dû créer un contexte de conversion de paramètres. Les notes des diverses contributions sur les informations MSDN de référence suggérant que le contexte de conversion de paramètres conserve la liste de tous les objets qu'il gère, un seul contexte, a suffi pour convertir les paramètres de toutes les chaînes.
marshal_context ctx;
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
ctx.marshal_as<const wchar_t*>(module),
...
Comme l'API C a besoin de connaître la longueur d'errorData, j'ai voulu calculer cette dernière et la passer comme paramètre. Tandis que je pouvais me contenter d'utiliser la méthode Length à partir de la chaîne managée, la longueur pouvait être en octets ou en caractères ; c'est pourquoi j'ai jeté un coup d'œil à l'exemple fourni dans la documentation. L'exemple utilisait la fonction C wcslen (le nombre de caractères double octet). Je décidai de commencer par convertir les paramètres des données, puis de soumettre la longueur à l'aide de la même fonction.
const wchar_t* eData;
eData = ctx.marshal_as<const wchar_t*>(errorData);
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
...
eData,
wcslen(eData),
…
Le paramètre final est un tableau de structures pour les données nom/valeur personnalisées pouvant être incluses dans le rapport de plantage. Bien qu'il soit possible de convertir les paramètres d'une structure managée en structure non managée, il semblait plus facile de créer une classe managée comme type de paramètre et d'effectuer une itération dans le tableau en le convertissant en type de l'API C avant de passer les données. Dans la signature de la méthode, vous noterez que le paramètre est une référence à un tableau de références aux objets (vous voyez les deux accents circonflexes ?). Cela est dû au fait que les objets sont des classes managées par opposition aux structures qui sont en général passées par valeur. Étant donné que, au moment de la compilation, nous ne connaissons pas le nombre d'éléments présents dans le tableau, nous allons devoir les allouer lors de l'exécution et les nettoyer quand nous aurons fini.
unsigned long fieldCount = 0;
ADP_CrashReportField* fields;
if(customFields != nullptr ) {
fieldCount = Convert::ToUInt32(customFields->Length);
if( fieldCount > 0 ) {
fields = new ADP_CrashReportField[fieldCount];
for( unsigned int i = 0; i < fieldCount; i++ ) {
CrashReportField^ crf = customFields[i];
String^ name = crf->Name;
String^ value = crf->Value;
fields[i].name =
(wchar_t*)ctx.marshal_as<const wchar_t*>(name);
fields[i].value =
(wchar_t*)ctx.marshal_as<const wchar_t*>(value);
}
}
}
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
…
fields,
fieldCount
);
delete[] fields;
Après avoir créé mes pointeurs et mes variables et après avoir validé que je ne vais pas lever d'exception de référence null, j'alloue un tableau de structures ADP_CrashReportField à partir de l'API C à l'aide de l'opérateur new. Remarque : sur la dernière ligne, après avoir fini d'appeler la méthode de l'API, je supprime l'allocation du tableau.
Pour une raison ou pour une autre, le compilateur a eu du mal à convertir les paramètres des chaînes name et value à partir des objets en ligne CrashReportField ; aussi ai-je dû les copier vers des références String managées. Puis, en utilisant le même contexte, je suis arrivé à convertir les paramètres vers une chaîne const native.
L'API C répertorie les deux champs dans la structure en tant que wchar_t*, sans le modificateur const. Cela implique que l'API a pu modifier les données de ces structures. Je ne pense pas qu'elle le fasse et la fonction marshal_as ne prend pas en charge la conversion de paramètres vers une chaîne native non const. C'est pourquoi je transtype en wchar_t* les valeurs retournées. C'est risqué. Si l'API devait modifier les données situées à ces pointeurs, une violation d'accès aurait toutes les chances de se produire.
Les touches finales
Étant donné que l'assemblage .NET sera principalement utilisé dans Visual Studio, j'ai commenté à fond toutes les classes, les méthodes, les propriétés, les énumérations, etc., la plupart du temps en recopiant la documentation utilisée dans le SDK C Language Reference. Les commentaires peuvent servir à générer de la documentation de référence et ils peuvent également s'afficher sous forme de suggestions pop-up lorsqu'on écrit le code.
Comme je suis quelqu’un d'assez perfectionniste, je suis allé aux ressources et j'ai changé en 0.91.1.* le numéro de version de cette DLL (l'astérisque indique au compilateur d'incrémenter le numéro de chaque build afin que je puisse détecter les changement de version lors des tests) de manière à ce qu'il corresponde à la version d'édition 0.91 du fichier lib que nous bundlons.
Test de l'API .NET
Tout au long de ce développement, j'avais besoin de tester les fonctions que je créais. Je voulais m'assurer qu'elles fonctionneraient à partir de C#, le langage qui a le plus de chances d'être utilisé par les développeurs. Dans le code source, vous verrez un second projet qui inclut une série de tests d'intégration exerçant toutes les fonctions et toutes les propriétés de la classe.
Ces tests utilisent NUnit mais ils peuvent très bien être portés vers le framework Visual Studio de tests unitaires.
Comme je veux tester que les fonctions marchent vraiment, ce sont des tests d'intégration qui envoient des données vers le service ATDS et qui vérifient les réponses retournées. Vous avez besoin pour exécuter les tests qu'ATDS soit en cours d'exécution. Cela veut dire qu'il n'existe pas de test distinct de la manière dont la bibliothèque .NET réagit lorsqu'ATDS n'est pas en cours d'exécution, mais le premier test retourne un message personnalisé d'échec dans ce cas de figure, ce qui vous permet de tester ce scénario dans un test distinct.
Les méthodes de l'API suivent l'API C et ne devraient pas vous désorienter si vous avez lu le SDK Developer Guide. Cela dit, les tests constituent une bonne occasion d'exercer chaque méthode et ils s'avèrent utiles si vous avez des doutes sur l'un des appels.
1 La conversion de paramètres (marshaling) consiste à transférer des données en toute sécurité entre deux systèmes disparates. Dans notre cas, nous devons faire attention à la manière dont la mémoire est gérée pour les pointeurs ou les types de référence comme les chaînes, les tableaux et les objets.
2 Il est tout à fait possible que les ID ADP changent dans le futur et ne correspondent plus à un System.Guid, auquel cas cela provoquerait une modification fondamentale dans l'API .NET. Mais, les GUID étant standard et fonctionnels, ce choix m'a paru procéder d'une hypothèse acceptable.
3 Vous trouverez un tableau des types et de leur contexte d'utilisation dans « Overview of Marshaling in C++ ».