Der AdpService für .NET

Seitdem die Entwickler-Plattform für Intel® Atom™ Prozessoren erstmals angekündigt wurde, herrscht reges Interesse an der Entwicklung von Apps in .NET. Anfang Dezember schrieb yllian-saint-hilare einen Blog-Beitrag über die Entwicklung von .NET-Anwendungen für den Shop. Der Beitrag erläuterte die Schritte, die zum Erstellen eines .NET-Wrappers um eine nicht verwaltete C++-DLL für das ADP-SDK erforderlich sind. Daraus gingen zwei DLLs hervor, die Sie in Ihre .NET-App einbeziehen können. Dieser Artikel basiert auf Yllians Arbeit, allerdings wird der Code umgeschrieben und vervollständigt, um eine einzige verwaltete DLL zu erstellen, die das gesamte ADP-SDK kapselt.

Hinweis: Diese Diskussion und der zugehörige Code beziehen sich auf das Intel ADP SDK Version 0.92.

Ich möchte nur den Code

Wenn Sie nur die DLL herunterladen und zum Entwickeln oder Portieren Ihrer Anwendung in das ADP nutzen möchten, finden Sie rechts den entsprechenden Link. Die bereitgestellte ZIP-Datei enthält die binäre DLL, die Sie in Ihre Anwendung einfügen können, um anschließend gleich loszulegen.

1. Erstellen Sie eine neue Windows-Forms-Anwendung.

2. Kopieren Sie die DLL in das Lösungsverzeichnis Ihrer Anwendung (oder noch besser in ein „lib“-Verzeichnis im Lösungsverzeichnis).

3. Fügen Sie in Visual Studio eine Referenz hinzu, indem Sie zur DLL-Datei navigieren.

4. Instanziieren Sie ein com.intel.adp.AdpWrapper-Objekt im Startcode Ihrer Anwendung und rufen Sie zumindest Initialize und IsAuthorized auf.

Weitere Informationen zur Entwicklung einer Anwendung mit diesem Code finden Sie im Artikel „Facebook-Beispiel für .NET“.

Erstellen der .NET-DLL für ADP

Die folgende Diskussion greift Yllians Arbeit auf, ohne die dieser Beitrag nicht möglich wäre. Ich hoffe, ich kann ein wenig Klarheit schaffen, wenn Sie dieses Projekt selbst erstellen und einrichten, die Bibliothek erweitern oder es für eine neue Version des ADP-SDK aktualisieren wollen.

Der Übersicht halber werde ich nur Teile des Codes genauer beleuchten. Sie können die gesamte Lösung über den rechts bereitgestellten Link herunterladen und den Code durchsehen, während Sie diesen Artikel lesen.

Die ADP-SDK-Bibliotheken werden als .lib-Dateien geliefert, die statisch mit Ihrem Projekt verlinkt werden sollten. Damit .NET auf den nicht verwalteten Code zugreifen kann, muss es eine DLL referenzieren können. Sie können nicht verwalteten Code statisch in eine verwaltete (d. h. .NET) DLL einbinden, aber nur wenn Sie in verwaltetem C++ programmieren.

Erstellen einer verwalteten DLL zur Referenzierung der ADP-Bibliothek

Um dieses Projekt von Grund auf neu zu erstellen, habe ich Visual Studio 2008 gestartet und ein neues „C++ CLR Class Library“-Projekt (AdpService) angelegt. Beachten Sie, dass dies eine CLR (Common Language Runtime) ist, die es der DLL ermöglicht, von .NET-Apps genutzt zu werden.

Da ich für jede ADP-SDK-Funktion eine verwaltete Methode verfügbar machen werde, habe ich adpcode.lib und adpcode.h aus dem SDK-Verzeichnis in das Verzeichnis kopiert, in dem sich dieses Projekt befindet, damit es vom Linker gefunden werden kann. Ich habe die C-API verwendet, da sie einfacher zu handhaben ist als ihr C++-Pendant, aber ich werde die SDK-Aufrufe über eine verwaltete Klasse verfügbar machen.

Um sicherzustellen, dass die Projektreferenzen bestehen, habe ich „adpcore.h“ als Include zur AppService.h-Datei hinzugefügt und als bestehendes Element zu den Headerdateien im Projekt.

Als letztes müssen wir den Linker anweisen, nach der Bibliothek zu suchen. Stellen Sie sicher, dass in den Projekteigenschaften rechts oben „All configurations“ ausgewählt ist. Wählen Sie anschließend Linker/Input und fügen Sie „adpcore.lib psapi.lib shlwapi.lib Advapi32.lib“ (alle vier werden benötigt) zum Eintrag „Additional Dependencies“ hinzu.

Erstellen einer .NET-Klasse für den Zugriff auf das ADP-System

Um den .NET-Code in nicht verwalteten Code (adpcore.lib) umzusetzen, müssen wird jede Funktion, typedef und enum duplizieren und die Daten zwischen den verwalteten und nicht verwalteten Typen marshallen1.

Ich habe eine „public enum class“, ReturnCode, in der Headerdatei erstellt, die die Enumeration repliziert, die sich in der C-Headerdatei, adpcore.h, befindet. Dieser Marshaller erfolgt direkt, da die Enumeration eine Ganzzahl mit Vorzeichen darstellt. Jeder Aufruf der C-Bibliothek-Funktionen gibt einen Wert zurück, den wir in einen Enum-Typ umwandeln können.

Die Funktionen der C-Bibliothek können statische Methoden der .NET-Klasse sein. Um unseren Code mit der ADP-Integration zu testen, werden wir jedoch den ADP-Dienst nachahmen. Daher sollte dies eine Klasse mit Instanzenmethode sein, hinter der sich eine Schnittstelle verbirgt.

Wenn Sie sich AdpService.h im Quellcode ansehen, werden Sie feststellen, dass alle Methodenaufrufe und die beiden nicht konstanten Eigenschaften nicht statisch sind.

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
    {
    ....
    }
};

Wenn Sie mit der bestehenden C++-API im SDK vertraut sind, werden Sie bemerken, dass Sie entweder aus dieser Klasse erben oder ein Objekt aus dieser Klasse verwenden können. Im Unterschied zum SDK gibt es hier keine sofortige Initialisierung und Autorisierung durch einen Konstruktor. Sie können dies gegebenenfalls hinzufügen, aber ich empfehle, dass Sie die ADP-Integration aus der Klasse Ihres Hauptprogramms als AdpService-Objekt erstellen (oder delegieren), und zwar aus drei Gründen:

  • 1. Ihre Anwendung ist kein Autorisierungsdienst, daher sollten Sie nicht rein basierend auf dem Kontext der Domäne Unterklassen erstellen.
  • 2. Durch die Verwendung von Unterklassen kann es vorkommen, dass der übergeordnete Konstruktor eine Ausnahme auslöst, die nicht ordnungsgemäß behandelt wird (Sie erhalten ein falsch initiiertes Objekt). Es gibt einen Forumbeitrag, der diese Problematik näher erläutert.
  • 3. Durch die Zusammensetzung können Sie eine Abhängigkeit verwenden und diese in Ihrer App während der Testphase durch ein Pseudoobjekt ersetzen, damit Sie die Tests automatisieren können, ohne ATDS auszuführen.

GUIDs konvertieren

Das SDK nutzt GUIDs als IDs für Anwendungen und Komponenten. Da es sich hierbei um Standard-GUIDs handelt und .NET einen integrierten Typ (System.Guid) hat, schien es eine gute Idee, diesen integrierten Typ zu verwenden 2. Die beiden Autorisierungsmethoden akzeptieren GUID-Typen in ihrer Signatur. Ich habe zwei Eigenschaften erstellt, um GUID-Typen für die Debug-IDs zurückzugeben, die für die Entwicklung und den Test von Anwendungen und Komponenten genutzt werden.

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);
    }
}

Der Code oben ermöglicht es uns, nicht verwaltete Daten in den verwalteten Speicher zu geben und sie in einen Typ zu konvertieren. Zuerst habe ich System.Guid als gültigen Rückgabetyp (d. h. nicht als Referenz, „GUID^“) verwendet, da es sich um eine Struktur und nicht um eine Klasse handelt.

Im Getter erstellen wir ein neues Byte-Array im verwalteten Speicher („gcnew“ weist darauf hin, dass es sich um eine Garbagecollection handelt). Wenn Sie mit dem verwalteten C++ von Microsoft nicht vertraut sind: Das ^ entspricht dem * (d. h. es weist auf eine Referenz hin), wird allerdings für verwaltete Referenzen verwendet.

Als nächstes erfolgt die Zuweisung eines Pointers auf den Anfang unseres Arrays. Er informiert den Marshaller, dass beim nicht verwalteten Aufruf derselbe Speicher verwendet werden soll, damit wir die Ergebnisse erhalten, wenn wir fertig sind.

Anschließend verwenden wir die C-Standardfunktion memcpy, um den Wert im ADP_DEBUG_APPLICTIONID-Pointer Byte für Byte in die C-Bibliothek in unser Byte-Array zu kopieren. Beachten Sie, dass ich zur Headerdatei hinzufügen muss, um diese Funktion einzubeziehen.

Abschließend erstellen wir einen GUID-Typ, indem wir das Byte-Array an den Konstruktor übergeben.

In diesem Zusammenhang ist es wichtig anzumerken, dass ich weiß, dass die ADP_DEBUG_APPLICATIONID-Daten 16 Byte lang sind. Wenn Ihr Build in einer anderen Version der C-API ausgeführt wird, sollten Sie diesen Wert überprüfen oder den Code entsprechend ändern.

Marshalling von Daten

In den Methodensignaturen oben sehen Sie, dass in die meisten Aufrufe keine Daten eingehen, was die Umsetzung vereinfacht. Noch nützlicher ist es, dass alle in den Aufruf eingehenden Daten Konstante sind, was bedeutet, dass die Funktionen die Daten nicht ändern. Dies vereinfacht das Daten-Marshalling, da wir es für unsere API-Aufrufe nicht fixieren müssen. (Wir können den Marshaller Kopien der Daten für den nicht verwalteten Code erstellen lassen.)

Wenn Sie den Quellcode ansehen, werden Sie feststellen, dass die einfachen API-Aufrufe, wie Initialize, einfach nur die C-API aufrufen und einen Wert zurückgeben, der in unseren ReturnCode-Typ umgesetzt wird:

ReturnCode AdpService::Initialize()
{
    return (ReturnCode)ADP_Initialize();
}

Jede Autorisierungsmethode nimmt einen GUID-Parameter und kehrt im Wesentlichen um, was wir oben mit den Eigenschaften gemacht haben. Sie konvertiert also die GUID in ein verwaltetes Byte-Array, kopiert es in den geeigneten nicht verwalteten Typ und übergibt diesen als Parameter an den C-API-Aufruf.

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);
} 

Die Konvertierung verwalteter Strings zu nicht verwalteten Strings ist kompliziert, da es in nicht verwaltetem C++ zahlreiche Stringtypen geben kann. Strings können aus einem oder zwei Bytes bestehen, mit Null enden (üblicherweise), änderbar (const) oder nicht änderbar sein usw. Das verwaltete C++ von Microsoft enthält Methoden, die das String-Marshalling vereinfachen. Durch das Hinzufügen von msclr::interop zu den Namespace-Referenzen können wir die marshal_as-Funktion für die Konvertierung von Stringdaten verwenden, wie von mir in der ApiVersion-Eigenschaft genutzt:

String^ AdpService::ApiVersion::get()
{
    return marshal_as<System::String^>(ADP_API_VERSION);
} 

Die marshal_as-Methode kann in einem statischen Kontext (d. h. als Funktion) aufgerufen werden, wenn, wie oben, native Strings in verwaltete Strings konvertiert werden. Beim Marshalling von nicht verwalteten Strings müssen Sie zuerst einen Marshall-Kontext erstellen, in dem das Marshalling des Strings erfolgen soll3. Das anspruchsvollste Datenmarshalling für diese API war die Absturzberichtfunktion, und zwar aufgrund der Anzahl und Komplexität der Parameter.

ReturnCode AdpService::ReportCrash( 
    String^ module, 
    unsigned long lineNumber, 
    String^ message, 
    String^ category, 
    String^ errorData, 
    array<CrashReportField^>^ customFields ) 
    

Der C-API-Aufruf verwendet diese sowie zwei weitere Parameter: die Länge des errorData-Strings und die Anzahl der Elemente im customFields-Array. Da beide im .NET-Kontext ermittelt werden konnten, habe ich sie im Aufruf ausgewertet, anstatt den Entwickler zu zwingen, sie einzubeziehen.

Für das Marshalling der Stringdaten aus verwalteten String-Referenzen zu nicht verwalteten „const wchar_t*“ musste ich einen Marshall-Kontext erzeugen. Die Community-Beiträge der MSDN-Referenz lassen darauf schließen, dass der Marshall-Kontext eine Liste aller von ihm behandelten Objekte behält. Daher war ein Kontext für das Marshalling aller Strings ausreichend.

marshal_context ctx;
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
    ctx.marshal_as<const wchar_t*>(module),
    ... 

Da die C-API die Länge von errorData benötigt, wollte ich den Wert berechnen und übergeben. Ich hätte zwar einfach die Length-Methode des verwalteten Strings verwenden können, aber der „length“-Wert könnte Bytes oder Zeichen enthalten, daher sah ich mir das Beispiel in der Dokumentation genauer an. Es verwendet die C-Funktion wcslen (die Anzahl der Doppelbyte-Zeichen). Ich entschied, zuerst das Daten-Marshalling durchzuführen und anschließend die Länge mit derselben Funktion zu übergeben.

const wchar_t* eData;
eData = ctx.marshal_as<const wchar_t*>(errorData);
...
ReturnCode rc = (ReturnCode)ADP_ReportCrash(
    ...
    eData, 
    wcslen(eData), 
    …

Der letzte Parameter ist ein Array von Strukturen für benutzerdefinierte Name/Wert-Daten, die im Absturzbericht enthalten sein können. Obwohl das Marshalling einer verwalteten Struktur in eine nicht verwaltete Struktur möglich ist, schien es einfacher, eine verwaltete Klasse als Parametertyp zu erstellen, das Array zu durchlaufen und es in den C-API-Typ zu konvertieren, bevor die Daten übergeben werden. Beachten Sie, dass der Parameter in der Methodensignatur eine Referenz auf ein Array von Referenzen auf diese Objekten ist (sehen Sie die beiden Caret-Zeichen?). Dies liegt daran, dass die Objekte verwaltete Klassen und nicht Strukturen sind, die generell über den Wert übergeben werden. Da wir die Anzahl der Elemente im Array zum Zeitpunkt der Kompilierung nicht kennen, müssen wir den Wert während der Laufzeit zuweisen und bereinigen, wenn wir fertig sind.

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;

Nachdem meine Pointer und Variablen erstellt sind und ich validiert habe, dass ich keine Nullreferenzausnahme auslöse, weise ich mit dem neuen Operator ein Array der ADP_CrashReportField-Struktur aus der C-API zu. Beachten Sie in der letzten Zeile, dass ich nach dem Ende des Aufrufs der API-Methode die Array-Zuweisung lösche.

Aus irgendeinem Grund wollte der Compiler das Marshalling der Namen- und Wertestrings aus den CrashReportField-Objekten nicht durchführen, daher musste ich sie zu verwalteten String-Referenzen kopieren. Dann konnte ich mit demselben Kontext zu einem nativen const-String marshallen.

Die C-API listet die beiden Felder in der Struktur als wchar_t* auf, ohne den „const“-Modifizierer. Das weist darauf hin, dass die API die Daten in diesen Strukturen ändern konnte. Ich denke nicht, dass sie dies tut, und die marshal_as-Funktion bietet keine Unterstützung für das Marshalling zu einem nativen Nicht-const-String. Daher wandelte ich die Rückgabewerte in „wchar_t*“ um. Das ist riskant: Sollte die API die Daten bei diesen Pointern ändern, würde dies wahrscheinlich eine Zugriffsverletzung auslösen.

Finetuning

Da das .NET-Assembly vorwiegend in Visual Studio verwendet werden wird, habe ich alle Klassen, Methoden, Eigenschaften, Aufzählungen und alles andere im Detail kommentiert, überwiegend durch das Kopieren der in der SDK C Language Reference verwendeten Dokumentation. Die Kommentare können verwendet werden, um eine Referenzdokumentation zu erstellen oder sie können auch als Intellisense-Vorschläge beim Schreiben des Codes eingeblendet werden.

Da ich ein Tüftler bin, habe ich in den Ressourcen die Versionsnummer dieser DLL in 0.91.1.* geändert (das Sternchen weist den Compiler an, den Wert beim Build zu erhöhen, so dass ich Versionsänderungen beim Testen erkennen kann), damit sie mit der 0.91-Release-Version der lib-Datei, die wir paketieren, übereinstimmt.

Testen der .NET-API

Während der Entwicklung musste ich die von mir erstellten Funktionen testen. Ich wollte sichergehen, dass sie von C# aus funktionieren, da dies wahrscheinlich die von den Entwicklern benutzte Sprache sein würde. Im Quellcode werden Sie ein zweites Projekt sehen, das eine Reihe von Integrationstests für alle Funktionen und Eigenschaften der Klasse enthält.

Diese Tests nutzen NUnit als Testlauf, könnten aber im Handumdrehen in das in Visual Studio integrierte Komponententest-Framework portiert werden.

Da ich testen möchte, ob die Funktionen wirklich funktionieren, handelt es sich um Integrationstests, die Daten an den ATDS-Dienst senden und die Antwort überprüfen. Für diese Tests muss ATDS ausgeführt werden. Das bedeutet, es gibt keinen eigenen Test für die Reaktion der .NET-Bibliothek, wenn ATDS nicht ausgeführt wird, aber der erste Test gibt in dem Fall eine benutzerdefinierte Fehlermeldung zurück, damit Sie dieses Szenario in einem eigenen Testlauf prüfen können.

Die API-Methoden folgen der C-API und sollten einfach und vertraut sein, wenn Sie den SDK-Entwicklerleitfaden gelesen haben. Allerdings bieten die Tests ein gutes Beispiel dafür, wie jede Methode ausgeführt wird und sie sollten hilfreich sein, wenn Sie sich mit einem der Aufrufe nicht auskennen.

1 Marshalling bezeichnet den Prozess des sicheren Datentransfers zwischen zwei verschiedenartigen Systemen. In diesem Fall müssen wir vorsichtig sein, wie der Speicher für Pointer oder Referenztypen, wie Strings, Arrays und Objekte, behandelt wird.

2 Zugegebenermaßen könnten sich die ADP-IDs in Zukunft ändern und nicht mehr länger mit der System.GUID übereinstimmen, was eine signifikante Umgestaltung der .NET-API erforderlich machen würde. Da GUIDs jedoch ein Standard sind und funktionieren, war dies meiner Meinung nach eine akzeptable Annahme.

3 In „Overview of Marshaling in C++“ finden Sie eine Matrix der Typen und Kontextverwendung.

0