Verwenden von C/C++-Bibliotheken mit Xamarin
Übersicht
Mit Xamarin können Entwickler plattformübergreifende native mobile Apps mit Visual Studio erstellen. Im Allgemeinen werden C#-Bindungen verwendet, um vorhandene Plattformkomponenten für Entwickler verfügbar zu machen. In einigen Fällen müssen Xamarin-Apps jedoch eine vorhandene Codebasis nutzen. Und nicht immer haben Teams die Zeit, das Budget oder die Ressourcen, um eine große, umfangreich getestete und umfassend optimierte Codebasis zu C# zu portieren.
Mit Visual C++ für plattformübergreifende Mobile-Entwicklung kann der C/C++- und C#-Code als Teil der gleichen Lösung erstellt werden. Dies bietet eine Vielzahl von Vorteilen, u.a. eine einheitliche Debuggingoberfläche. Microsoft hat C/C++ und Xamarin auf diese Weise verwendet, um Apps wie Hyperlapse Mobile und die Pix-Kamera bereitzustellen.
In einigen Fällen ist es jedoch wünschenswert oder erforderlich, vorhandene C/C++-Tools und -Prozesse beizubehalten und den Bibliothekscode weiterhin von der Anwendung zu entkoppeln. Dabei wird die Bibliothek so behandelt, als handle es sich um eine Drittanbieterkomponente. In diesen Szenarien besteht die Herausforderung nicht nur darin, die relevanten Member für C# verfügbar zu machen, sondern auch darin, die Bibliothek als Abhängigkeit zu verwalten. Und selbstverständlich soll dieser Prozess soweit wie möglich automatisiert werden.
In diesem Beitrag wird ein allgemeiner Ansatz für dieses Szenario erläutert und anhand eines einfachen Beispiels veranschaulicht.
Hintergrund
C/C++ wird als plattformübergreifende Sprache betrachtet. Es muss jedoch mit großer Sorgfalt sichergestellt werden, dass der Quellcode tatsächlich plattformübergreifend ist. Dabei müssen nur C/C++-Elemente verwendet werden, die von allen Zielcompilern unterstützt werden und wenig oder gar keinen über Bedingungen eingebundenen Plattformcode bzw. compilerspezifischen Code umfassen.
Letztendlich muss der Code auf allen Zielplattformen kompiliert und erfolgreich ausgeführt werden können. Entscheidend ist daher die Gemeinsamkeit aller Zielplattformen (und Zielcompiler). Dennoch können aufgrund von geringfügigen Unterschieden bei den Compilern Probleme auftreten. Ein entscheidender Faktor, weshalb umfangreiche Tests (vorzugsweise automatisiert) auf den einzelnen Zielplattformen immer wichtiger werden.
Übergeordnete Vorgehensweise
Die folgende Abbildung zeigt den vierstufigen Ansatz, mit dem C/C++-Quellcode in eine plattformübergreifende Xamarin-Bibliothek umgewandelt wird, die über NuGet freigegeben und dann in einer Xamarin.Forms-App verwendet wird.
Die vier Phasen sind:
- Kompilieren des C/C++-Quellcodes in plattformspezifische native Bibliotheken.
- Umschließen der nativen Bibliotheken mit einer Visual Studio-Projektmappe.
- Verpacken und Übertragen eines NuGet-Pakets per Push für den .NET-Wrapper.
- Verwenden des NuGet-Pakets aus einer Xamarin-App.
Phase 1: Kompilieren des C/C++-Quellcodes in plattformspezifische native Bibliotheken
Das Ziel dieser Phase besteht darin, native Bibliotheken zu erstellen, die vom C#-Wrapper aufgerufen werden können. Dies kann je nach Situation relevant sein oder nicht. Leider ist es im Rahmen dieses Artikels nicht möglich, auf alle Tools und Prozesse einzugehen, die für dieses gängige Szenario verwendet werden können. Wichtige Überlegungen betreffen die Synchronisierung der C/C++-Codebasis mit nativem Wrappercode, ausreichende Unittests sowie die Buildautomatisierung.
Die Bibliotheken in dieser exemplarischen Vorgehensweise wurden mit Visual Studio Code und einem begleitenden Shellskript erstellt. Diese exemplarische Vorgehensweise und das Beispiel sind im Mobile CAT GitHub-Repository näher beschrieben. Die nativen Bibliotheken werden in diesem Fall als Drittanbieterabhängigkeit behandelt. Die Veranschaulichung dieser Phase dient lediglich zur Bereitstellung von Kontextinformationen.
Der Einfachheit halber wird in der exemplarischen Vorgehensweise nur eine Teilmenge der Architekturen als Ziel verwendet. Für iOS wird das Hilfsprogramm lipo verwendet, um eine einzige FAT-Binärdatei aus den einzelnen architekturspezifischen Binärdateien zu erstellen. Android verwendet dynamische Binärdateien mit der Erweiterung .so, iOS verwendet eine statische FAT-Binärdatei mit der Erweiterung .a.
Phase 2: Umschließen der nativen Bibliotheken mit einer Visual Studio-Projektmappe
In der nächsten Phase werden die nativen Bibliotheken umschlossen, damit sie problemlos von .NET verwendet werden können. Zu diesem Zweck kommt eine Visual Studio-Projektmappe mit vier Projekten zum Einsatz. Der gemeinsame Code ist in einem freigegebenen Projekt enthalten. Durch verschiedene Projekte, in denen je Xamarin.Android, Xamarin.iOS und .NET Standard als Ziele definiert sind, kann plattformunabhängig auf die Bibliothek verwiesen werden.
Der Wrapper verwendet die „Bait-and-Switch-Technik“. Dies ist nicht die einzige Möglichkeit, durch diese Vorgehensweise kann jedoch problemlos auf die Bibliothek verwiesen werden. Außerdem ist keine explizite Verwaltung plattformspezifischer Implementierungen innerhalb der Anwendung selbst erforderlich. Durch diese Technik wird im Wesentlichen sichergestellt, dass die Ziele (.NET Standard, Android, iOS) denselben Namespace, denselben Assemblynamen und dieselbe Klassenstruktur verwenden. Da NuGet immer eine plattformspezifische Bibliothek bevorzugt, wird die .NET Standard-Version nie zur Laufzeit verwendet.
Bei den wesentlichen Aufgaben dieses Schritts geht es um die Verwendung von P/Invoke, um die Methoden der nativen Bibliothek aufzurufen, und um die Verwaltung der Verweise auf die zugrunde liegenden Objekte. Das Ziel besteht darin, die Funktionalität der Bibliothek für den Consumer verfügbar zu machen und dabei jegliche Komplexität zu abstrahieren. Xamarin.Forms-Entwickler müssen nicht mit den internen Vorgängen der nicht verwalteten Bibliothek vertraut sein. Entwickler sollten keinen Unterschied zur Verwendung einer verwalteten C#-Bibliothek bemerken.
Das Ergebnis dieser Phase ist eine Reihe von .NET-Bibliotheken (eine Bibliothek pro Ziel) sowie ein NUSPEC-Dokument, das die erforderlichen Informationen zum Erstellen des Pakets im nächsten Schritt enthält.
Phase 3: Verpacken und Übertragen eines NuGet-Pakets per Push für den .NET-Wrapper
In der dritten Phase wird mithilfe der Buildartefakte aus dem vorherigen Schritt ein NuGet-Paket erstellt. Das Ergebnis dieses Schritts ist ein NuGet-Paket, das von einer Xamarin-App verwendet werden kann. In der exemplarischen Vorgehensweise wird ein lokales Verzeichnis verwendet, das als NuGet-Feed fungiert. In einer Produktionsumgebung sollte mit diesem Schritt ein Paket in einem öffentlichen oder privaten NuGet-Feed veröffentlicht werden, und der Schritt sollte vollständig automatisiert erfolgen.
Phase 4: Verwenden des NuGet-Pakets aus einer Xamarin.Forms-App
Der letzte Schritt besteht darin, aus einer Xamarin.Forms-App auf das NuGet-Paket zu verweisen und dieses zu verwenden. Dazu muss der NuGet-Feed in Visual Studio so konfiguriert werden, dass er den im vorherigen Schritt definierten Feed verwendet.
Nachdem der Feed konfiguriert wurde, muss von jedem Projekt in der plattformübergreifenden Xamarin.Forms-App auf das Paket verwiesen werden. Da mit der „Bait-and-Switch-Technik“ identische Schnittstellen bereitgestellt werden, kann die Funktionalität der nativen Bibliothek mithilfe von Code aufgerufen werden, der an einem einzigen Ort definiert ist.
Das Quellcoderepository umfasst eine Liste mit weiteren Informationen, die u.a. Artikel zum Einrichten eines privaten NuGet-Feeds in Azure DevOps und zum Übertragen des Pakets per Push an diesen Feed enthält. Wenngleich der Zeitaufwand für die Einrichtung etwas höher ist als bei Verwendung eines lokalen Verzeichnisses, eignet sich diese Art von Feed besser für eine Teamentwicklungsumgebung.
Exemplarische Vorgehensweise
Die aufgeführten Schritte gelten für Visual Studio für Mac, lassen sich jedoch auch auf Visual Studio 2017 übertragen.
Voraussetzungen
Zur Ausführung der beschriebenen Schritte wird Folgendes benötigt:
Hinweis
Für die Bereitstellung von Apps für iPhones wird ein aktives Apple Developer-Konto benötigt.
Erstellen von nativen Bibliotheken (Phase 1)
Die Funktionalität der nativen Bibliothek basiert auf dem Beispiel aus der exemplarischen Vorgehensweise: Erstellen und Verwenden einer statischen Bibliothek (C++).
In dieser exemplarischen Vorgehensweise wird die erste Phase – das Erstellen der nativen Bibliotheken – übersprungen, da die Bibliothek in diesem Szenario als Drittanbieterabhängigkeit definiert ist. Die vorkompilierten nativen Bibliotheken werden gemeinsam mit dem Beispielcode hinzugefügt oder können direkt heruntergeladen werden.
Arbeiten mit der nativen Bibliothek
Das ursprüngliche Beispiel MathFuncsLib umfasst eine einzelne Klasse MyMathFuncs
mit der folgenden Definition:
namespace MathFuncs
{
class MyMathFuncs
{
public:
double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);
};
}
Eine weitere Klasse definiert Wrapperfunktionen, mit denen ein .NET-Consumer die native MyMathFuncs
-Klasse erstellen und löschen bzw. mit dieser Klasse interagieren kann.
#include "MyMathFuncs.h"
using namespace MathFuncs;
extern "C" {
MyMathFuncs* CreateMyMathFuncsClass();
void DisposeMyMathFuncsClass(MyMathFuncs* ptr);
double MyMathFuncsAdd(MyMathFuncs *ptr, double a, double b);
double MyMathFuncsSubtract(MyMathFuncs *ptr, double a, double b);
double MyMathFuncsMultiply(MyMathFuncs *ptr, double a, double b);
double MyMathFuncsDivide(MyMathFuncs *ptr, double a, double b);
}
Diese Wrapperfunktionen werden Xamarin-seitig verwendet.
Umschließen der nativen Bibliothek (Phase 2)
Für diese Phase werden die vorkompilierten Bibliotheken benötigt, die im vorherigen Abschnitt beschrieben sind.
Erstellen der Visual Studio-Projektmappe
Klicken Sie in Visual Studio für Mac auf Neues Projekt (auf der Willkommensseite) oder auf Neue Projektmappe (im Menü Datei).
Wählen Sie im Fenster Neues Projekt die Option Freigegebenes Projekt (Multi-Plattform> > Bibliothek) aus, und klicken Sie dann auf Weiter.
Aktualisieren Sie die folgenden Felder, und klicken Sie auf Erstellen:
- Projektname: MathFuncs.Shared
- Projektmappenname: MathFuncs
- Standort: Verwenden Sie den Standardspeicherort (oder wählen Sie einen alternativen Speicherort aus)
- Projektverzeichnis im Projektmappenverzeichnis erstellen: Aktivieren Sie diese Option
Doppelklicken Sie im Projektmappen-Explorer auf das Projekt MathFuncs.Shared, und wechseln Sie zu Haupteinstellungen.
Entfernen Sie .Shared aus dem Standardnamespace, sodass dieser lediglich auf MathFuncs festgelegt ist. Klicken Sie dann auf OK.
Öffnen Sie MyClass.cs (von der Vorlage erstellt), benennen Sie sowohl die Klasse als auch den Dateinamen in MyMathFuncsWrapper um, und ändern Sie den Namespace in MathFuncs.
Klicken Sie bei gedrückter STRG-Taste auf die Projektmappe MathFuncs, und wählen Sie im Menü Hinzufügen die Option Neues Projekt hinzufügen... aus.
Wählen Sie im Fenster Neues Projekt die Option .NET Standard-Bibliothek (Multi-Plattform> > Bibliothek) aus, und klicken Sie dann auf Weiter.
Wählen Sie .NET Standard 2.0 aus, und klicken Sie auf Weiter.
Aktualisieren Sie die folgenden Felder, und klicken Sie auf Erstellen:
- Projektname: MathFuncs.Standard
- Standort: Verwenden Sie denselben Speicherort wie beim freigegebenen Projekt.
Doppelklicken Sie im Projektmappen-Explorer auf das Projekt MathFuncs.Standard.
Wechseln Sie zu Haupteinstellungen, und ändern Sie den Standardnamespace in MathFuncs.
Wechseln Sie zu den Ausgabeoptionen, und ändern Sie Assemblyname in MathFuncs.
Wechseln Sie zu den Compilereinstellungen, ändern Sie die Konfiguration in Release, legen Sie für Debuginformationen die Option Nur Symbole fest, und klicken Sie dann auf OK.
Löschen Sie Class1.cs/Getting Started aus dem Projekt (sofern diese Einstellung als Teil der Vorlage hinzugefügt wurde).
Klicken Sie bei gedrückter STRG-Taste auf den Projektordner Abhängigkeiten/Verweise, und wählen Sie Verweise bearbeiten aus.
Wählen Sie auf der Registerkarte Projekte den Eintrag MathFuncs.Shared aus, und klicken Sie auf OK.
Wiederholen Sie die Schritte 7-17 (ignorieren Sie dabei Schritt 9) mit folgenden Konfigurationen:
PROJEKTNAME VORLAGENNAME MENÜ „NEUES PROJEKT“ MathFuncs.Android Klassenbibliothek Android > Bibliothek MathFuncs.iOS Bindungsbibliothek iOS > Bibliothek Doppelklicken Sie im Projektmappen-Explorer auf das Projekt MathFuncs.Android, und wechseln Sie zu den Compilereinstellungen.
Wählen Sie für die Konfiguration die Option Debug aus, und fügen Sie Android; zu Symbole definieren hinzu.
Ändern Sie die Einstellung von Konfiguration in Release, und fügen Sie Android; zu Symbole definieren hinzu.
Wiederholen Sie die Schritte 19-20 für MathFuncs.iOS. Legen Sie für Symbole definieren dabei in beiden Fällen fest, dass iOS; anstelle von Android; hinzugefügt wird.
Erstellen Sie die Projektmappe in der Konfiguration Release (STRG + BEFEHL + B), und überprüfen Sie, ob alle drei Ausgabeassemblys (Android, iOS, .NET Standard) (in den jeweiligen bin-Ordnern des Projekts) dieselbe MathFuncs.dll verwenden.
Zu diesen Zeitpunkt sollte die Projektmappe über drei Ziele (Android, iOS und .NET Standard) sowie ein freigegebenes Projekt verfügen, auf das jedes dieser drei Ziele verweist. In der Konfiguration der Ziele sollten derselbe Standardnamespace und Ausgabeassemblys mit identischem Namen festgelegt sein. Dies ist für die zuvor erwähnte „Bait-and-Switch-Technik“ erforderlich.
Hinzufügen nativer Bibliotheken
Bei den erforderlichen Schritten zum Hinzufügen von nativen Bibliotheken zum Wrapper müssen bei Android und iOS geringfügige Unterschiede berücksichtigt werden.
Native Verweise für MathFuncs.Android
Klicken Sie bei gedrückter STRG-Taste auf das Projekt MathFuncs.Android, und wählen Sie im Menü Hinzufügen die Option Neuer Ordner aus. Legen Sie als Ordnernamen lib fest.
Klicken Sie bei gedrückter STRG-Taste für jede ABI (Application Binary Interface) auf den Ordner lib, wählen Sie im Menü Hinzufügen die Option Neuer Ordner aus, und geben Sie als Ordnernamen den Namen der jeweiligen ABI ein. In diesem Fall:
- arm64 v8a
- armeabi-v7a
- x86
- x86_64
Hinweis
Eine detailliertere Übersicht finden Sie im Thema Architekturen und CPUs im NDK-Entwicklerhandbuch. Lesen Sie insbesondere den Abschnitt zu nativem Code in App-Paketen.
Überprüfen Sie die Ordnerstruktur:
- lib - arm64-v8a - armeabi-v7a - x86 - x86_64
Fügen Sie basierend auf der folgenden Zuordnung die entsprechenden .so-Bibliotheken zu jedem ABI-Ordner hinzu:
arm64-v8a: lib/Android/arm64
armeabi-v7a: lib/Android/arm
x86: lib/Android/x86
x86_64: lib/Android/x86_64
Hinweis
Zum Hinzufügen von Dateien klicken Sie bei gedrückter STRG-Taste auf den Ordner der jeweiligen ABI, und wählen Sie im Menü Hinzufügen den Eintrag Dateien hinzufügen... aus. Wählen Sie die geeignete Bibliothek aus (im Verzeichnis PrecompiledLibs), klicken Sie auf Öffnen, und klicken Sie dann auf OK (behalten Sie die Standardoption Kopieren der Datei in das Verzeichnis bei).
Klicken Sie bei gedrückter STRG-Taste auf jede .so-Datei, und wählen Sie die Option EmbeddedNativeLibrary aus dem Menü Buildvorgang aus.
Der Ordner lib sollte nun wie folgt aussehen:
- lib
- arm64-v8a
- libMathFuncs.so
- armeabi-v7a
- libMathFuncs.so
- x86
- libMathFuncs.so
- x86_64
- libMathFuncs.so
Native Verweise für MathFuncs.iOS
Klicken Sie bei gedrückter STRG-Taste auf das Projekt MathFuncs.iOS, und wählen Sie im Menü Hinzufügen die Option Nativen Verweis hinzufügen aus.
Wählen Sie die Bibliothek libMathFuncs.a aus (unter „libs/ios“ im Verzeichnis PrecompiledLibs), und klicken Sie auf Öffnen
Klicken Sie bei gedrückter STRG-Taste auf die Datei libMathFuncs (im Ordner Native Verweise), und wählen Sie die Option Eigenschaften aus dem Menü aus
Konfigurieren Sie die Eigenschaften von Native Verweise, indem Sie sie im Eigenschaftenpad aktivieren (gesetztes Häkchen):
- Laden erzwingen
- Ist C++
- Intelligenter Link
Hinweis
Durch die Verwendung eines Bindungsbibliothek-Projekttyps mit einem nativen Verweis wird die statische Bibliothek eingebettet. Außerdem kann sie automatisch mit der Xamarin.iOS-App verknüpft werden, die auf die Bibliothek verweist (selbst wenn sie über ein NuGet-Paket eingeschlossen wird).
Öffnen Sie ApiDefinition.cs, löschen Sie den kommentierten Vorlagencode (behalten Sie lediglich den
MathFuncs
-Namespace bei), und führen Sie denselben Schritt für Structs.cs aus.Hinweis
Diese Dateien (mit den Buildvorgängen ObjCBindingApiDefinition und ObjCBindingCoreSource) werden zum Erstellen eines Bindungsbibliotheksprojekts benötigt. Wir schreiben den Code für den Aufruf unserer nativen Bibliothek jedoch außerhalb dieser Dateien und zwar so, dass er von den Android- und iOS-Zielen über einen Standardaufruf von P/Invoke gemeinsam verwendet werden kann.
Schreiben des verwalteten Bibliothekscodes
Schreiben Sie nun den C#-Code, um die native Bibliothek aufzurufen. Ziel dieses Schritts ist, die zugrunde liegende Komplexität zu verbergen. Der Consumer sollte sich nicht mit den internen Abläufen der nativen Bibliothek oder den Konzepten von P/Invoke auskennen müssen.
Erstellen eines SafeHandle
Klicken Sie bei gedrückter STRG-Taste auf das Projekt MathFuncs.Shared, und wählen Sie im Menü Hinzufügen die Option Datei hinzufügen... aus.
Wählen Sie im Fenster Neue Datei die Option Leere Klasse aus, weisen Sie den Namen MyMathFuncsSafeHandle zu, und klicken Sie dann auf Neu
Implementieren Sie die Klasse MyMathFuncsSafeHandle:
using System; using Microsoft.Win32.SafeHandles; namespace MathFuncs { internal class MyMathFuncsSafeHandle : SafeHandleZeroOrMinusOneIsInvalid { public MyMathFuncsSafeHandle() : base(true) { } public IntPtr Ptr => handle; protected override bool ReleaseHandle() { // TODO: Release the handle here return true; } } }
Hinweis
Ein SafeHandle ist die bevorzugte Methode, um mit nicht verwalteten Ressourcen in verwaltetem Code zu arbeiten. Auf diese Weise wird ein großer Teil der Codebausteine abstrahiert, die sich auf die kritische Finalisierung und den Objektlebenszyklus beziehen. Der Besitzer dieses Handles kann ihn anschließend wie jede andere verwaltete Ressource behandeln und muss nicht das vollständige Dispose-Muster implementieren.
Erstellen der internen Wrapperklasse
Öffnen Sie MyMathFuncsWrapper.cs, und ändern Sie die Klasse in eine interne statische Klasse.
namespace MathFuncs { internal static class MyMathFuncsWrapper { } }
Fügen Sie in derselben Datei die folgende Bedingungsanweisung zur Klasse hinzu:
#if Android const string DllName = "libMathFuncs.so"; #else const string DllName = "__Internal"; #endif
Hinweis
Dadurch wird der Wert der Konstante DllName abhängig davon festgelegt, ob die Bibliothek für Android oder für iOS erstellt wird. So wird den unterschiedlichen Namenskonventionen der Plattformen Rechnung getragen, aber auch dem in diesem Fall verwendeten Bibliothekstyp. Android verwendet eine dynamische Bibliothek und erwartet daher einen Dateinamen mit Erweiterung. Da wir eine statische Bibliothek verwenden, ist für iOS „ __Internal“ erforderlich.
Fügen Sie am Anfang der Datei MyMathFuncsWrapper.cs einen Verweis auf System.Runtime.InteropServices hinzu.
using System.Runtime.InteropServices;
Fügen Sie die Wrappermethoden zum Erstellen und Löschen der Klasse MyMathFuncs hinzu:
[DllImport(DllName, EntryPoint = "CreateMyMathFuncsClass")] internal static extern MyMathFuncsSafeHandle CreateMyMathFuncs(); [DllImport(DllName, EntryPoint = "DisposeMyMathFuncsClass")] internal static extern void DisposeMyMathFuncs(MyMathFuncsSafeHandle ptr);
Hinweis
Wir übergeben unsere Konstante DllName gemeinsam mit EntryPoint an das Attribut DllImport. „EntryPoint“ gibt den Namen der Funktion, die innerhalb dieser Bibliothek aufgerufen werden soll, explizit an die .NET-Laufzeit weiter. Technisch gesehen müssen wir den EntryPoint-Wert nicht angeben, wenn die Namen der verwalteten Methoden mit den Namen der nicht verwalteten Methoden identisch sind. Wenn kein Name angegeben wird, wird stattdessen der Name der verwalteten Methode als EntryPoint verwendet. Die explizite Angabe wird jedoch empfohlen.
Fügen Sie die Wrappermethoden hinzu, damit wir unter Verwendung des folgenden Codes mit der Klasse MyMathFuncs arbeiten können:
[DllImport(DllName, EntryPoint = "MyMathFuncsAdd")] internal static extern double Add(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsSubtract")] internal static extern double Subtract(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsMultiply")] internal static extern double Multiply(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsDivide")] internal static extern double Divide(MyMathFuncsSafeHandle ptr, double a, double b);
Hinweis
Für die Parameter in diesem Beispiel werden einfache Typen verwendet. Da das Marshalling eine bitweise Kopie ist, ist in diesem Fall keine zusätzliche Arbeit erforderlich. Beachten Sie auch die Verwendung der Klasse MyMathFuncsSafeHandle anstelle der Standardklasse IntPtr. IntPtr wird dem SafeHandle automatisch als Teil des Marshallingvorgangs zugeordnet.
Überprüfen Sie, ob die fertig gestellte Klasse MyMathFuncsWrapper wie unten gezeigt aussieht:
using System.Runtime.InteropServices; namespace MathFuncs { internal static class MyMathFuncsWrapper { #if Android const string DllName = "libMathFuncs.so"; #else const string DllName = "__Internal"; #endif [DllImport(DllName, EntryPoint = "CreateMyMathFuncsClass")] internal static extern MyMathFuncsSafeHandle CreateMyMathFuncs(); [DllImport(DllName, EntryPoint = "DisposeMyMathFuncsClass")] internal static extern void DisposeMyMathFuncs(MyMathFuncsSafeHandle ptr); [DllImport(DllName, EntryPoint = "MyMathFuncsAdd")] internal static extern double Add(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsSubtract")] internal static extern double Subtract(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsMultiply")] internal static extern double Multiply(MyMathFuncsSafeHandle ptr, double a, double b); [DllImport(DllName, EntryPoint = "MyMathFuncsDivide")] internal static extern double Divide(MyMathFuncsSafeHandle ptr, double a, double b); } }
Fertigstellen der Klasse „MyMathFuncsSafeHandle“
Öffnen Sie die Klasse MyMathFuncsSafeHandle, und wechseln Sie zum Platzhalterkommentar TODO innerhalb der Methode ReleaseHandle:
// TODO: Release the handle here
Ersetzen Sie die Zeile TODO:
MyMathFuncsWrapper.DisposeMyMathFuncs(this);
Erstellen der Klasse „MyMathFuncs“
Nachdem der Wrapper abgeschlossen wurde, erstellen Sie im nächsten Schritt eine Klasse „MyMathFuncs“, die den Verweis auf das nicht verwaltete C++-Objekt „MyMathFuncs“ verwaltet.
Klicken Sie bei gedrückter STRG-Taste auf das Projekt MathFuncs.Shared, und wählen Sie im Menü Hinzufügen die Option Datei hinzufügen... aus.
Wählen Sie im Fenster Neue Datei die Option Leere Klasse aus, weisen Sie den Namen MyMathFuncs zu, und klicken Sie dann auf Neu.
Fügen Sie der Klasse MyMathFuncs die folgenden Member hinzu:
readonly MyMathFuncsSafeHandle handle;
Implementieren Sie den Konstruktor für die Klasse so, dass beim Instanziieren der Klasse ein Handle für das native MyMathFuncs-Objekt erstellt und gespeichert wird:
public MyMathFuncs() { handle = MyMathFuncsWrapper.CreateMyMathFuncs(); }
Implementieren Sie die IDisposable-Schnittstelle mithilfe des folgenden Codes:
public class MyMathFuncs : IDisposable { ... protected virtual void Dispose(bool disposing) { if (handle != null && !handle.IsInvalid) handle.Dispose(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } // ... }
Implementieren Sie die MyMathFuncs-Methoden mithilfe der MyMathFuncsWrapper-Klasse, um die zugrunde liegenden Vorgänge auszuführen. Dabei wird der Zeiger auf das zugrunde liegende nicht verwaltete Objekt übergeben, den wir gespeichert haben. Der Code sollte wie folgt aussehen:
public double Add(double a, double b) { return MyMathFuncsWrapper.Add(handle, a, b); } public double Subtract(double a, double b) { return MyMathFuncsWrapper.Subtract(handle, a, b); } public double Multiply(double a, double b) { return MyMathFuncsWrapper.Multiply(handle, a, b); } public double Divide(double a, double b) { return MyMathFuncsWrapper.Divide(handle, a, b); }
Erstellen der NUSPEC-Datei
Um die Bibliothek zu verpacken und über NuGet zu verteilen, wird eine NUSPEC-Datei benötigt. In dieser Datei ist festgelegt, welche der resultierenden Assemblys für die verschiedenen unterstützten Plattformen hinzugefügt werden.
Klicken Sie bei gedrückter STRG-Taste auf die Projektmappe MathFuncs, wählen Sie im Menü Hinzufügen die Option Projektmappenordner hinzufügen aus, und weisen Sie den Namen SolutionItems zu.
Klicken Sie bei gedrückter STRG-Taste auf den Ordner SolutionItems, und wählen Sie im Menü Hinzufügen die Option Neue Datei... aus.
Wählen Sie im Fenster Neue Datei die Option Leere XML-Datei aus, weisen Sie den Namen MathFuncs.nuspec zu, und klicken Sie dann auf Neu.
Aktualisieren Sie MathFuncs.nuspec mit den grundlegenden Paketmetadaten, die für den NuGet-Consumer angezeigt werden sollen. Beispiel:
<?xml version="1.0"?> <package> <metadata> <id>MathFuncs</id> <version>$version$</version> <authors>Microsoft Mobile Customer Advisory Team</authors> <description>Sample C++ Wrapper Library</description> <requireLicenseAcceptance>false</requireLicenseAcceptance> <copyright>Copyright 2018</copyright> </metadata> </package>
Hinweis
In der NUSPEC-Referenz finden Sie weitere Einzelheiten zum Schema, das für dieses Manifest verwendet wird.
Fügen Sie ein
<files>
-Element als untergeordnetes Element des<package>
-Elements hinzu (direkt unterhalb von<metadata>
), um jede Datei mit einem separaten<file>
-Element zu identifizieren:<files> <!-- Android --> <!-- iOS --> <!-- netstandard2.0 --> </files>
Hinweis
Wenn ein Paket in einem Projekt installiert wird und mehrere Assemblys mit demselben Namen angegeben sind, wählt NuGet effektiv die Assembly aus, die für die jeweilige Plattform am spezifischsten ist.
Fügen Sie die
<file>
-Elemente für die Android-Assemblys hinzu:<file src="MathFuncs.Android/bin/Release/MathFuncs.dll" target="lib/MonoAndroid81/MathFuncs.dll" /> <file src="MathFuncs.Android/bin/Release/MathFuncs.pdb" target="lib/MonoAndroid81/MathFuncs.pdb" />
Fügen Sie die
<file>
-Elemente für die iOS-Assemblys hinzu:<file src="MathFuncs.iOS/bin/Release/MathFuncs.dll" target="lib/Xamarin.iOS10/MathFuncs.dll" /> <file src="MathFuncs.iOS/bin/Release/MathFuncs.pdb" target="lib/Xamarin.iOS10/MathFuncs.pdb" />
Fügen Sie die
<file>
-Elemente für die netstandard2.0-Assemblys hinzu:<file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.dll" target="lib/netstandard2.0/MathFuncs.dll" /> <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.pdb" target="lib/netstandard2.0/MathFuncs.pdb" />
Überprüfen Sie das NUSPEC-Manifest:
<?xml version="1.0"?> <package> <metadata> <id>MathFuncs</id> <version>$version$</version> <authors>Microsoft Mobile Customer Advisory Team</authors> <description>Sample C++ Wrapper Library</description> <requireLicenseAcceptance>false</requireLicenseAcceptance> <copyright>Copyright 2018</copyright> </metadata> <files> <!-- Android --> <file src="MathFuncs.Android/bin/Release/MathFuncs.dll" target="lib/MonoAndroid81/MathFuncs.dll" /> <file src="MathFuncs.Android/bin/Release/MathFuncs.pdb" target="lib/MonoAndroid81/MathFuncs.pdb" /> <!-- iOS --> <file src="MathFuncs.iOS/bin/Release/MathFuncs.dll" target="lib/Xamarin.iOS10/MathFuncs.dll" /> <file src="MathFuncs.iOS/bin/Release/MathFuncs.pdb" target="lib/Xamarin.iOS10/MathFuncs.pdb" /> <!-- netstandard2.0 --> <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.dll" target="lib/netstandard2.0/MathFuncs.dll" /> <file src="MathFuncs.Standard/bin/Release/netstandard2.0/MathFuncs.pdb" target="lib/netstandard2.0/MathFuncs.pdb" /> </files> </package>
Hinweis
Diese Datei gibt die Assemblyausgabepfade aus einem Release-Build an. Stellen Sie daher sicher, dass Sie die Projektmappe mit dieser Konfiguration erstellen.
An diesem Punkt enthält die Projektmappe drei .NET-Assemblys und ein unterstützendes NUSPEC-Manifest.
Verteilen des .NET-Wrappers mit NuGet
Im nächsten Schritt wird das NuGet-Paket verpackt und verteilt, damit es problemlos von der App genutzt und als Abhängigkeit verwaltet werden kann. Die Umschließung und Nutzung könnte über eine einzige Projektmappe erfolgen, das Verteilen der Bibliothek über NuGet ist jedoch für die Entkopplung nützlich und ermöglicht es uns, jede Codebasis separat zu verwalten.
Vorbereiten eines lokalen Paketverzeichnisses
Die einfachste Form eines NuGet-Feeds ist ein lokales Verzeichnis:
- Wechseln Sie im Finder zu einem beliebigen Verzeichnis. Beispiel: /Benutzer.
- Wählen Sie im Menü Datei die Option Neuer Ordner aus, und geben Sie einen aussagekräftigen Namen wie z.B. local-nuget-feed ein.
Erstellen des Pakets
Legen Sie für die Buildkonfiguration die Option Release fest, und führen Sie den Build über BEFEHL + B aus.
Öffnen Sie Terminal, und wechseln Sie in den Ordner, der die NUSPEC-Datei enthält.
Führen Sie in Terminal den Befehl nuget pack aus, und geben Sie dabei die NUSPEC-Datei, die Version (z.B. 1.0.0) sowie das OutputDirectory an (den im vorherigen Schritt erstellten Ordner, local-nuget-feed). Beispiel:
nuget pack MathFuncs.nuspec -Version 1.0.0 -OutputDirectory ~/local-nuget-feed
Überprüfen Sie, ob MathFuncs.1.0.0.nupkg im Verzeichnis local-nuget-feed erstellt wurde.
[OPTIONAL] Verwenden eines privaten NuGet-Feeds mit Azure DevOps
Ein zuverlässigeres Verfahren wird unter Erste Schritte mit NuGet-Paketen in Azure DevOps beschrieben. In diesem Artikel erfahren Sie, wie Sie einen privaten Feed erstellen und das im vorherigen Schritt generierte Paket per Push an diesen Feed übertragen.
Idealerweise wird dieser Workflow vollständig automatisiert, beispielsweise mithilfe von Azure Pipelines. Weitere Informationen finden Sie unter Erste Schritte mit Azure Pipelines.
Verwenden des .NET-Wrappers aus einer Xamarin.Forms-App
Zum Abschluss der exemplarischen Vorgehensweise erstellen Sie eine Xamarin.Forms-App, die das soeben im lokalen NuGet-Feed veröffentlichte Paket verwendet.
Erstellen des Xamarin.Forms-Projekts
Öffnen Sie eine neue Instanz von Visual Studio für Mac. Dieser Schritt kann über Terminal ausgeführt werden:
open -n -a "Visual Studio"
Klicken Sie in Visual Studio für Mac auf Neues Projekt (auf der Willkommensseite) oder auf Neue Projektmappe (im Menü Datei).
Wählen Sie im Fenster Neues Projekt die Option Leere Forms-App (Multi-Plattform > >App) aus, und klicken Sie dann auf Weiter.
Aktualisieren Sie die folgenden Felder, und klicken Sie auf Weiter:
- App-Name: MathFuncsApp.
- Organisations-ID: Verwenden Sie einen Reverse-Namespace, z.B. com.{Ihre_Org} .
- Zielplattformen: Verwenden Sie die Standardeinstellung (sowohl Android- als auch iOS-Ziele).
- Freigegebener Code: Legen Sie für diese Einstellung .NET Standard fest (eine Projektmappe mit freigegebener Bibliothek ist möglich, kann im Rahmen dieser exemplarischen Vorgehensweise jedoch nicht behandelt werden).
Aktualisieren Sie die folgenden Felder, und klicken Sie auf Erstellen:
- Projektname: MathFuncsApp.
- Projektmappenname: MathFuncsApp.
- Standort: Verwenden Sie den Standardspeicherort (oder wählen Sie einen alternativen Speicherort aus).
Klicken Sie bei gedrückter STRG-Taste im Projektmappen-Explorer auf das Ziel (MathFuncsApp.Android oder MathFuncs.iOS) für die ersten Tests, und wählen Sie dann Als Startprojekt festlegen.
Wählen Sie das bevorzugte Gerät oder den bevorzugten Simulator/Emulator aus.
Führen Sie die Projektmappe aus (BEFEHL + EINGABE), um zu überprüfen, ob das Xamarin.Forms-Vorlagenprojekt ordnungsgemäß erstellt und ausgeführt wird.
Hinweis
iOS (insbesondere der Simulator) weist tendenziell die kürzeste Zeit für das Erstellen/Bereitstellen auf.
Hinzufügen des lokalen NuGet-Feeds zur NuGet-Konfiguration
Wählen Sie in Visual Studio die Option Einstellungen aus (im Visual Studio-Menü).
Wählen Sie im Abschnitt NuGet die Option Quellen aus, und klicken Sie auf Hinzufügen.
Aktualisieren Sie die folgenden Felder, und klicken Sie auf Quelle hinzufügen:
- Name: Geben Sie einen aussagekräftigen Namen ein, z.B. „Local-Packages“.
- Standort: Geben Sie den im vorherigen Schritt erstellten Ordner local-nuget-feed an.
Hinweis
In diesem Fall muss kein Benutzername und kein Kennwort angegeben werden.
Klicken Sie auf OK.
Verweisen auf das Paket
Wiederholen Sie die folgenden Schritte für alle Projekte (MathFuncsApp, MathFuncsApp.Android und MathFuncsApp.iOS).
- Klicken Sie bei gedrückter STRG-Taste auf das Projekt, und wählen Sie im Menü Hinzufügen die Option NuGet-Pakete hinzufügen... aus.
- Suchen Sie nach MathFuncs.
- Überprüfen Sie, ob die Version des Pakets 1.0.0 ist und auch die übrigen Details wie erwartet angezeigt werden (z.B. Titel und Beschreibung, also MathFuncs und C++-Beispielwrapperbibliothek).
- Wählen Sie das Paket MathFuncs aus, und klicken Sie auf Paket hinzufügen.
Verwenden der Bibliotheksfunktionen
Nachdem Sie einen Verweis auf das MathFuncs-Paket in jedem Projekt erstellt haben, können die Funktionen vom C#-Code verwendet werden.
Öffnen Sie MainPage.xaml.cs aus dem gemeinsamen Xamarin.Forms-Projekt MathFuncsApp (auf das sowohl von MathFuncsApp.Android als auch von MathFuncsApp.iOS verwiesen wird).
Fügen Sie am Anfang der Datei using-Anweisungen für System.Diagnostics und MathFuncs hinzu:
using System.Diagnostics; using MathFuncs;
Deklarieren Sie oben in der
MainPage
-Klasse eine Instanz derMyMathFuncs
-Klasse:MyMathFuncs myMathFuncs;
Setzen Sie die Methoden
OnAppearing
undOnDisappearing
aus derContentPage
-Stammklasse außer Kraft:protected override void OnAppearing() { base.OnAppearing(); } protected override void OnDisappearing() { base.OnDisappearing(); }
Aktualisieren Sie die Methode
OnAppearing
, um die zuvor deklarierte VariablemyMathFuncs
zu initialisieren:protected override void OnAppearing() { base.OnAppearing(); myMathFuncs = new MyMathFuncs(); }
Aktualisieren Sie die Methode
OnDisappearing
, um die MethodeDispose
fürmyMathFuncs
aufzurufen:protected override void OnDisappearing() { base.OnAppearing(); myMathFuncs.Dispose(); }
Implementieren Sie wie nachfolgend beschrieben eine private Methode TestMathFuncs:
private void TestMathFuncs() { var numberA = 1; var numberB = 2; // Test Add function var addResult = myMathFuncs.Add(numberA, numberB); // Test Subtract function var subtractResult = myMathFuncs.Subtract(numberA, numberB); // Test Multiply function var multiplyResult = myMathFuncs.Multiply(numberA, numberB); // Test Divide function var divideResult = myMathFuncs.Divide(numberA, numberB); // Output results Debug.WriteLine($"{numberA} + {numberB} = {addResult}"); Debug.WriteLine($"{numberA} - {numberB} = {subtractResult}"); Debug.WriteLine($"{numberA} * {numberB} = {multiplyResult}"); Debug.WriteLine($"{numberA} / {numberB} = {divideResult}"); }
Rufen Sie am Ende der
OnAppearing
-MethodeTestMathFuncs
auf:TestMathFuncs();
Führen Sie die App auf jeder Zielplattform aus, und überprüfen Sie, ob die Ausgabe im Anwendungsausgabepad wie folgt aussieht:
1 + 2 = 3 1 - 2 = -1 1 * 2 = 2 1 / 2 = 0.5
Hinweis
Wenn beim Testen unter Android eine DLLNotFoundException-Ausnahme oder unter iOS ein Buildfehler auftritt, sollten Sie überprüfen, ob die CPU-Architektur des verwendeten Geräts/Emulators/Simulators mit der ausgewählten unterstützten Teilmenge kompatibel ist.
Zusammenfassung
In diesem Artikel wurde erläutert, wie Sie eine Xamarin.Forms-App erstellen, die native Bibliotheken über einen gemeinsamen .NET-Wrapper verwendet, der über ein NuGet-Paket bereitgestellt wird. Das in dieser exemplarischen Vorgehensweise verwendete Beispiel ist absichtlich sehr einfach gehalten, um die Vorgehensweise besser veranschaulichen zu können. Bei einer echten Anwendung müssen komplexe Aspekte wie die Ausnahmebehandlung, Rückrufe oder das Marshallen komplexerer Typen sowie Verknüpfungen mit weiteren Abhängigkeitsbibliotheken berücksichtigt werden. Ein wichtiger Aspekt ist der Prozess, über den die Entwicklung des C++-Codes koordiniert und mit den Wrapper- und Clientanwendungen synchronisiert wird. Dieser Prozess kann variieren, je nachdem, ob ein einzelnes Team für einen oder beide dieser Aspekte verantwortlich ist. In jedem Fall bietet die Automatisierung entscheidende Vorteile. Im Folgenden sind eine Reihe von Ressourcen aufgeführt, in denen Sie weitere Informationen zu einigen der wichtigsten Konzepte sowie zu den relevanten Downloads finden.