Reagieren Sie auf Native- und C ++ - Integration für iOS und Android

Kürzlich wurde mir angeboten, an einem interessanten Projekt zu arbeiten. Es war erforderlich, eine mobile Anwendung für den amerikanischen Start unter iOS und Android mit React Native zu entwickeln. Das wichtigste technische Merkmal und der Faktor, der meine Teilnahme an dem Projekt eindeutig entschieden hat, war die Integration einer in C ++ geschriebenen Bibliothek. Für mich könnte dies eine neue Erfahrung und ein neuer professioneller Test sein.

Warum musste die C ++ - Bibliothek integriert werden?


Diese Anwendung war für die Zwei-Faktor-Authentifizierung mit den Protokollen FIDO UAF und U2F unter Verwendung biometrischer Daten wie Face ID und Touch ID sowie ähnlicher Technologien für die Android-Plattform erforderlich. Der Client für die Authentifizierung war bereits bereit. Es war eine in C ++ geschriebene Bibliothek, die von einigen anderen Clients zusätzlich zur mobilen Anwendung verwendet wurde. Daher musste ich es auf ähnliche Weise in eine mobile Anwendung auf React Native integrieren.

Wie ich es gemacht habe


Es gibt einen Ansatz zur Integration von C ++ in die React Native-App von Facebook. Das Problem ist jedoch, dass es nur für die iOS-Plattform funktioniert und nicht klar ist, was in diesem Fall mit Android zu tun ist. Ich wollte das Problem für zwei Plattformen gleichzeitig lösen.

Ein Zweig des Dropbox Djinni-Tools , mit dem Sie plattformübergreifende Typdeklarationen generieren können. Im Wesentlichen handelt es sich um eine einfache mobile React Native-App mit einer benutzerdefinierten Verbindung zu Djinni. Ich habe es als Grundlage genommen.

Der Einfachheit halber ist der Anwendungscode in zwei Git-Repositorys unterteilt. Der erste speichert den Quellcode der React Native-Anwendung und der zweite enthält Djinni und die erforderlichen Abhängigkeiten.

Weitere Schritte


Zuerst müssen Sie eine Schnittstelle für die Interaktion von C ++ und React Native-Code deklarieren. In Dschinn wird dies mithilfe von IDL-Dateien durchgeführt. Öffnen Sie die Datei react-native-cpp-support / idl / main.Djinni im Projekt und machen Sie sich mit deren Struktur vertraut.

Für unsere Bequemlichkeit wurden bereits einige JavaScript-Datentypen und Bindungen für diese im Projekt deklariert. Daher können wir ohne zusätzliche Beschreibung mit den Typen String, Array, Map, Promise und anderen arbeiten.

Im Beispiel sieht diese Datei folgendermaßen aus:

DemoModule = interface +r { const EVENT_NAME: string = "DEMO_MODULE_EVENT"; const STRING_CONSTANT: string = "STRING"; const INT_CONSTANT: i32 = 13; const DOUBLE_CONSTANT: f64 = 13.123; const BOOL_CONSTANT: bool = false; testPromise(promise: JavascriptPromise); testCallback(callback: JavascriptCallback); testMap(map: JavascriptMap, promise: JavascriptPromise); testArray(array: JavascriptArray, callback: JavascriptCallback); testBool(value: bool, promise: JavascriptPromise); testPrimitives(i: i32, d: f64, callback: JavascriptCallback); testString(value: string, promise: JavascriptPromise); testEventWithArray(value: JavascriptArray); testEventWithMap(value: JavascriptMap); } 

Nachdem Sie Änderungen an der Schnittstellendatei vorgenommen haben, müssen Sie die Java / Objective-C / C ++ - Schnittstellen neu generieren. Dies ist einfach, indem Sie das Skript generate_wrappers.sh aus dem Ordner react-native-cpp-support / idl / ausführen . Dieses Skript sammelt alle Anzeigen aus unserer IDL-Datei und erstellt die entsprechenden Schnittstellen für sie. Es ist sehr praktisch.

Im Beispiel gibt es zwei C ++ - Dateien, die uns interessieren. Die erste enthält eine Beschreibung und die zweite Implementierung einfacher C ++ - Methoden:

react-native-cpp / cpp / DemoModuleImpl.hpp
react-native-cpp / cpp / DemoModuleImpl.cpp

Betrachten Sie den Code einer der Methoden als Beispiel:

 void DemoModuleImpl::testString(const std::string &value, const std::shared_ptr<::JavascriptPromise> &promise) { promise->resolveObject(JavascriptObject::fromString("Success!")); } 

Beachten Sie, dass das Ergebnis nicht mit dem Schlüsselwort return zurückgegeben wird , sondern mit dem JavaScriptPromise- Objekt, das vom letzten Parameter übergeben wurde, wie in der IDL- Datei beschrieben.

Nun wurde klar, wie man den notwendigen Code in C ++ beschreibt. Aber wie kann man in einer React Native-App damit interagieren? Zum Verständnis öffnen Sie einfach die Datei aus dem Ordner "react-native-cpp / index.js" , in dem alle im Beispiel beschriebenen Funktionen aufgerufen werden.

Die Funktion aus unserem Beispiel wird in JavaScript wie folgt aufgerufen:

 import { NativeAppEventEmitter, NativeModules... } from 'react-native'; const DemoModule = NativeModules.DemoModule; .... async promiseTest() { this.appendLine("testPromise: " + await DemoModule.testPromise()); this.appendLine("testMap: " + JSON.stringify(await DemoModule.testMap({a: DemoModule.INT_CONSTANT, b: 2}))); this.appendLine("testBool: " + await DemoModule.testBool(DemoModule.BOOL_CONSTANT)); // our sample function this.appendLine("testString: " + await DemoModule.testString(DemoModule.STRING_CONSTANT)); } 

Jetzt ist klar, wie die Testfunktionen auf der Seite von C ++ und JavaScript funktionieren. Ebenso können Sie den Code anderer Funktionen hinzufügen. Weiter werde ich überlegen, wie Android- und iOS-Projekte mit C ++ zusammenarbeiten.

Reagiere auf Native und C ++ für Android


Für die Interaktion von Android und C ++ müssen Sie das NDK installieren. Detaillierte Anweisungen dazu finden Sie unter developer.android.com/ndk/guides
Anschließend müssen Sie in der Datei react-native-cpp / android / app / build.gradle die folgenden Einstellungen hinzufügen:

 android { ... defaultConfig { ... ndk { abiFilters "armeabi-v7a", "x86" } externalNativeBuild { cmake { cppFlags "-std=c++14 -frtti -fexceptions" arguments "-DANDROID_TOOLCHAIN=clang", "-DANDROID_STL=c++_static" } } } externalNativeBuild { cmake { path "CMakeLists.txt" } } sourceSets { main { java.srcDirs 'src/main/java', '../../../react-native-cpp-support/support-lib/java' } } splits { abi { reset() enable enableSeparateBuildPerCPUArchitecture universalApk false // If true, also generate a universal APK include "armeabi-v7a", "x86" } } ... } 

Wir haben gradle so konfiguriert, dass die Anwendung für die verwendeten Architekturen erstellt wird, und die erforderlichen Build-Flags für cmake hinzugefügt, die CMAkeLists- Datei angegeben, die wir in Zukunft beschreiben werden, und auch die Java-Klassen von Djinni hinzugefügt, die wir verwenden werden.
Der nächste Schritt beim Einrichten eines Android-Projekts ist die Beschreibung der Datei CMakeLists.txt . In fertiger Form kann es entlang des Pfades react-native-cpp / android / app / CMakeLists.txt angezeigt werden .

 cmake_minimum_required(VERSION 3.4.1) set( PROJECT_ROOT "${CMAKE_SOURCE_DIR}/../.." ) set( SUPPORT_LIB_ROOT "${PROJECT_ROOT}/../react-native-cpp-support/support-lib" ) file( GLOB JNI_CODE "src/main/cpp/*.cpp" "src/main/cpp/gen/*.cpp" ) file( GLOB PROJECT_CODE "${PROJECT_ROOT}/cpp/*.cpp" "${PROJECT_ROOT}/cpp/gen/*.cpp" ) file( GLOB PROJECT_HEADERS "${PROJECT_ROOT}/cpp/*.hpp" "${PROJECT_ROOT}/cpp/gen/*.hpp" ) file( GLOB DJINNI_CODE "${SUPPORT_LIB_ROOT}/cpp/*.cpp" "${SUPPORT_LIB_ROOT}/jni/*.cpp" ) file( GLOB DJINNI_HEADERS "${SUPPORT_LIB_ROOT}/cpp/*.hpp" "${SUPPORT_LIB_ROOT}/jni/*.hpp" ) include_directories( "${SUPPORT_LIB_ROOT}/cpp" "${SUPPORT_LIB_ROOT}/jni" "${PROJECT_ROOT}/cpp" "${PROJECT_ROOT}/cpp/gen" ) add_library( # Sets the name of the library. native-lib # Sets the library as a shared library. SHARED ${JNI_CODE} ${DJINNI_CODE} ${DJINNI_HEADERS} ${PROJECT_CODE} ${PROJECT_HEADERS} ) 

Hier haben wir die relativen Pfade zur Unterstützungsbibliothek angegeben und Verzeichnisse mit dem erforderlichen C ++ - und JNI-Code hinzugefügt.

Ein weiterer wichtiger Schritt ist das Hinzufügen von DjinniModulesPackage zu unserem Projekt. Dazu geben wir in der Datei react-native-cpp / android / app / src / main / java / com / rncpp / jni / DjinniModulesPackage.java Folgendes an:

 ... import com.rncpp.jni.DjinniModulesPackage; ... public class MainApplication extends Application implements ReactApplication { ... @Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new DjinniModulesPackage() ); } ... } 

Das letzte wichtige Detail ist die Beschreibung der DjinniModulesPackage- Klasse, die wir gerade in der Hauptklasse unserer Anwendung verwendet haben. Es befindet sich im Pfad react-native-cpp / android / app / src / main / java / com / rncpp / jni / DjinniModulesPackage.java und enthält den folgenden Code:

 package com.rncpp.jni; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class DjinniModulesPackage implements ReactPackage { static { System.loadLibrary("native-lib"); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { List<NativeModule> modules = new ArrayList<>(); modules.add(new DemoModule(reactContext)); return modules; } } 

Von größtem Interesse für die obige Klasse ist die Zeile System.loadLibrary ("native-lib"); Dank dessen laden wir die Bibliothek mit unserem nativen Code und Djinni-Code in die Android-Anwendung.

Um zu verstehen, wie dies funktioniert, empfehle ich Ihnen, sich mit dem jni-Code aus dem Ordner vertraut zu machen, der ein jni-Wrapper für die Arbeit mit der Funktionalität unseres Moduls ist. Die Benutzeroberfläche ist in der IDL-Datei beschrieben.

Wenn die Android-Entwicklungsumgebung und React Native konfiguriert sind, können Sie das React Native-Projekt unter Android erstellen und ausführen. Führen Sie dazu zwei Befehle im Terminal aus:

npm installieren
npm laufen android

Hurra! Unser Projekt funktioniert!

Und wir sehen das folgende Bild auf dem Bildschirm des Android-Emulators (anklickbar):



Nun wollen wir sehen, wie iOS und React Native mit C ++ funktionieren.

Reagiere auf Native und C ++ für iOS


Öffnen Sie das React-Native-CPP-Projekt in Xcode.

Fügen Sie zunächst aus der Support-Bibliothek Links zu dem im Objective-C- und C ++ - Projekt verwendeten Code hinzu. Übertragen Sie dazu den Inhalt der Ordner " react-native-cpp-support / support-lib / objc /" und " react-native-cpp-support / support-lib / cpp /" in das Xcode- Projekt. Infolgedessen werden Ordner mit der Codeunterstützungsbibliothek im Projektstrukturbaum angezeigt (die Bilder können angeklickt werden):





Daher haben wir dem Projekt Beschreibungen von JavaScript-Typen aus der Support-Bibliothek hinzugefügt.

Der nächste Schritt besteht darin, die generierten Objective-C-Wrapper für unser Test-C ++ - Modul hinzuzufügen. Wir müssen den Code aus dem Ordner react-native-cpp / ios / rncpp / Generated / in das Projekt übertragen.

Es bleibt noch der C ++ - Code unseres Moduls hinzuzufügen, für den wir den Code aus den Ordnern react-native-cpp / cpp / und react-native-cpp / cpp / gen / in das Projekt übertragen.

Infolgedessen sieht der Projektstrukturbaum wie folgt aus (das Bild kann angeklickt werden):



Sie müssen sicherstellen, dass die hinzugefügten Dateien in der Liste "Quellen kompilieren" auf der Registerkarte "Phasen erstellen" angezeigt werden.



(das Bild ist anklickbar)

Der letzte Schritt besteht darin, den Code der Datei AppDelegate.m zu ändern, um die Initialisierung des Djinni-Moduls beim Start der Anwendung zu starten. Und dafür müssen Sie folgende Codezeilen ändern:

 ... #import "RCDjinniModulesInitializer.h" ... @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { ... id<RCTBridgeDelegate> moduleInitialiser = [[RCDjinniModulesInitializer alloc] initWithURL:jsCodeLocation]; RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:moduleInitialiser launchOptions:nil]; RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"rncpp" initialProperties: nil]; ... } 

Starten Sie jetzt unsere App auf iOS. (das Bild ist anklickbar)




Die Anwendung funktioniert!

Hinzufügen einer C ++ - Bibliothek zu unserem Projekt.


Zum Beispiel verwenden wir die beliebte OpenSSL-Bibliothek.

Und fangen wir mit Android an.

Wir klonen das Repository mit der bereits erstellten OpenSSL-Bibliothek für Android.

Fügen Sie die OpenSSL-Bibliothek in die Datei CMakeLists.txt ein:

 .... SET(OPENSSL_ROOT_DIR /Users/andreysaleba/projects/OpenSSL-for-Android-Prebuilt/openssl-1.0.2) SET(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/${ANDROID_ABI}/lib") SET(OPENSSL_INCLUDE_DIR ${OPENSSL_ROOT_DIR}/include) SET(OPENSSL_LIBRARIES "ssl" "crypto") ... LINK_DIRECTORIES(${OPENSSL_LIBRARIES_DIR} ${ZLIB_LIBRARIES_DIR}) include_directories( "${SUPPORT_LIB_ROOT}/cpp" "${SUPPORT_LIB_ROOT}/jni" "${PROJECT_ROOT}/cpp" "${PROJECT_ROOT}/cpp/gen" "${OPENSSL_INCLUDE_DIR}" ) add_library(libssl STATIC IMPORTED) add_library(libcrypto STATIC IMPORTED) ... set_target_properties( libssl PROPERTIES IMPORTED_LOCATION ${OPENSSL_LIBRARIES_DIR}/libssl.a ) set_target_properties( libcrypto PROPERTIES IMPORTED_LOCATION ${OPENSSL_LIBRARIES_DIR}/libcrypto.a ) target_link_libraries(native-lib PRIVATE libssl libcrypto) 

Anschließend fügen wir unserem C ++ - Modul einen einfachen Funktionscode hinzu, der die Version der OpenSSL-Bibliothek zurückgibt.

Fügen Sie in der Datei react-native-cpp / cpp / DemoModuleImpl.hpp Folgendes hinzu:

 void getOpenSSLVersion(const std::shared_ptr<::JavascriptPromise> & promise) override; 

Fügen Sie in der Datei react-native-cpp / cpp / DemoModuleImpl.cpp Folgendes hinzu:

 #include <openssl/crypto.h> ... void DemoModuleImpl::getOpenSSLVersion(const std::shared_ptr<::JavascriptPromise> &promise) { promise->resolveString(SSLeay_version(1)); } 

Es bleibt die Schnittstelle der neuen Funktion in der IDL-Datei "react-native-cpp-support / idl / main.djinni" zu beschreiben :

  getOpenSSLVersion(promise: JavascriptPromise); 

Wir rufen das Skript "generate_wrappers.sh" aus dem Ordner "react-native-cpp-support / idl /" auf .

Dann rufen wir in JavaScript die neu erstellte Funktion auf:

 async promiseTest() { ... this.appendLine("openSSL version: " + await DemoModule.getOpenSSLVersion()); } 

Alles ist bereit für Android.
Fahren wir mit iOS fort.

Wir klonen das Repository mit der zusammengestellten Version der OpenSSL-Bibliothek für iOS.

Öffnen Sie das iOS-Projekt in Xcode und fügen Sie in den Einstellungen auf der Registerkarte Build Settings den Pfad zur openssl-Bibliothek im Feld Other C Flags hinzu (ein Beispiel für den Pfad auf meinem Computer finden Sie unten):
-I / Users / andreysaleba / projects / prebuilt-openssl / dist / openssl-1.0.2d-ios / include

Fügen Sie im Feld Andere Linker-Flags die folgenden Zeilen hinzu:

 -L/Users/andreysaleba/projects/prebuilt-openssl/dist/openssl-1.0.2d-ios/lib -lcrypto -lssl 

Alles ist fertig. OpenSSL-Bibliothek für beide Plattformen hinzugefügt.

Dank für das Ansehen!

Source: https://habr.com/ru/post/de439676/


All Articles