Хабрахабр

[Из песочницы] Интеграция React Native и C++ для iOS и Android

Недавно мне предложили поработать над одним интересным проектом. Требовалось разработать мобильное приложение для американского стартапа на платформах iOS и Android с помощью React Native. Ключевой технической особенностью и фактором, который однозначно решил мое участие в проекте, стала задача интегрировать библиотеку, написанную на языке С++. Для меня это могло быть новым опытом и новым профессиональным испытанием.

Почему было необходимо интегрировать С++ библиотеку

Данное приложение было необходимо для двухфакторной аутентификации с помощью протоколов FIDO UAF и U2F, использующих биометрические данные, таких как Face ID и Touch ID, и аналогичных технологий для Android платформы. Клиент для аутентификации был уже готов. Это была библиотека, написанная на С++ и применяемая некоторыми другими клиентами помимо мобильного приложения. Так что от меня требовалось встроить ее аналогичным образом в мобильное приложение на React Native.

Как я это делал

Существует подход для интеграции С++ в React Native приложение от Facebook. Однако проблема в том, что он работает только для платформы iOS, и не понятно, что делать с Android в данном случае. Мне же хотелось решить проблему сразу для двух платформ.

По сути он является простым мобильным приложением на React Native с настроенной связью с Djinni. Форк инструмента Djinni от Dropbox, который позволяет генерировать кросс-платформенные объявления типов. Именно его я взял за основу.

В первом хранится исходный код React Native приложения, а во втором – Djinni и необходимые зависимости. Для удобства код приложения разбит на два git-репозитория.

Дальнейшие шаги

Сначала необходимо объявить интерфейс взаимодействия С++ и React Native кода. В Djinni это делается с помощью .idl файлов. Откроем файл react-native-cpp-support/idl/main.Djinni в проекте и ознакомимся с его структурой.

Таким образом, мы можем работать с типами String, Array, Map, Promise и другими без какого-либо дополнительного их описания. В проекте для нашего удобства уже объявлены некоторые типы данных JavaScript и биндинги для них.

В примере этот файл выглядит так:

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

После внесения изменений в файл интерфейсов необходимо перегенерировать Java/Objective-C/C++ интерфейсы. Это легко сделать запустив скрипт generate_wrappers.sh из папки react-native-cpp-support/idl/. Этот скрипт соберет все объявления из нашего idl файла и создаст соответствующие интерфейсы для них, это очень удобно.

Первый содержит описание, а второй реализацию простых С++ методов: В примере есть два интересующих нас С++ файла.

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

Рассмотрим код одного из методов в качестве примера:

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

Обратите внимание, что результат возвращается не с помощью keyword return, а с помощью объекта JavaScriptPromise, переданного последним параметром, как и описано в idl файле.

Но как взаимодействовать с этим в React Native приложении? Теперь стало понятно, как описывать необходимый код в С++. Чтобы понять, достаточно открыть файл из папки react-native-cpp/index.js, где вызываются все описанные в примере функции.

Функция из нашего примера вызывается в JavaScript следующим образом:

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

Теперь понятно, как работают тестовые функции на стороне С++ и JavaScript. Аналогичным образом можно добавить и код любых других функций. Дальше я рассмотрю, как работают Android и iOS проекты вместе с С++.

React Native и С++ для Android

Для взаимодействия Android и С++ необходимо установить NDK. Подробная инструкция, как это сделать, есть по ссылке developer.android.com/ndk/guides
Затем внутри файла react-native-cpp/android/app/build.gradle необходимо добавить следующие настройки:

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" } } ... }

Только что мы сконфигурировали gradle для сборки приложения для используемых архитектур и добавили необходимые build флаги для cmake, указали файл CMAkeLists, который опишем в дальнейшем, а также добавили java-классы из Djinni, которые будем использовать.
Следующий шаг настройки Android-проекта – описание файла CMakeLists.txt. В готовом виде его можно посмотреть по пути react-native-cpp/android/app/CMakeLists.txt.

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

Здесь мы указали относительные пути до support library, добавили директории с необходимым кодом С++ и JNI.

Для этого в файле react-native-cpp/android/app/src/main/java/com/rncpp/jni/DjinniModulesPackage.java укажем: Еще одним важным шагом является добавление DjinniModulesPackage в наш проект.

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

Последней важной деталью является описание класса DjinniModulesPackage, который мы только что использовали в главном классе нашего приложения. Он находится по пути react-native-cpp/android/app/src/main/java/com/rncpp/jni/DjinniModulesPackage.java и содержит следующий код:

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

Наибольший интерес в вышеописанном классе представляет собой строка System.loadLibrary(«native-lib»);, благодаря которой мы загружаем в Android-приложение библиотеку с нашим нативным кодом и кодом Djinni.

Для понимания, как это работает, советую ознакомиться с jni-кодом из папки, который представляет собой jni-обертку для работы с функционалом нашего модуля, а его интерфейс описан в idl-файле.

Для этого выполним две команды в терминале: В результате, если настроена среда разработки Android и React Native, можно собрать и запустить React Native проект на Android.

npm install
npm run android

Наш проект работает! Ура!

И мы видим следующую картинку на экране Android-эмулятора (кликабельна):

Теперь рассмотрим, как работают iOS и React Native с С++.

React Native и С++ для iOS

Откроем react-native-cpp проект в XCode.

Для этого перенесем содержимое папок react-native-cpp-support/support-lib/objc/ и react-native-cpp-support/support-lib/cpp/ в XCode проект. Сначала добавим ссылки на используемый в проекте Objective-C и С++ код из support library. В результате в дереве структуры проекта будут отображены папки с кодом support library (картинки кликабельны):

Таким образом, мы добавили описания JavaScript типов из support library в проект.

Нам потребуется перенести в проект код из папки react-native-cpp/ios/rncpp/Generated/. Следующий шаг – добавление сгенерированных objective-c оберток для нашего тестового С++ модуля.

Осталось добавить С++ код нашего модуля, для чего перенесем в проект код из папок react-native-cpp/cpp/ и react-native-cpp/cpp/gen/.

В итоге дерево структуры проекта будет выглядеть следующим образом (картинка кликабельна):

Нужно убедиться, что добавленные файлы появились в списке Compile Sources внутри табы Build Phases.

(картинка кликабельна)

А для этого потребуется изменить следующие строки кода: Последний шаг – изменить код файла AppDelegate.m, чтобы запустить инициализацию модуля Djinni при запуске приложения.

...
#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]; ...
}

Теперь запустим наше приложение на iOS. (картинка кликабельна)

Приложение работает!

Добавление библиотеки C++ библиотеки в наш проект.

Для примера используем популярную библиотеку OpenSSL.

И начнем с Android.

Клонируем репозиторий с уже собранной библиотекой OpenSSL для Android.

Включим в файл CMakeLists.txt библиотеку OpenSSL:

.... 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)

Затем добавим в наш С++ модуль код простой функции, возвращающий версию библиотеки OpenSSL.

В файл react-native-cpp/cpp/DemoModuleImpl.hpp добавим:

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

В файл react-native-cpp/cpp/DemoModuleImpl.cpp добавим:

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

Осталось описать интерфейс новой функции в idl-файле `react-native-cpp-support/idl/main.djinni`:

getOpenSSLVersion(promise: JavascriptPromise);

Вызываем скрипт `generate_wrappers.sh` из папки `react-native-cpp-support/idl/`.

Затем в JavaScript вызываем только что созданную функцию:

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

Для Android все готово.
Перейдем к iOS.

Клонируем репозиторий с собранной версией библиотеки OpenSSL для iOS.

0. Открываем iOS проект в XCode и в настройках в табе Build Settings добавляем путь к библиотеке openssl в поле Other C Flags (пример пути на моем компьютере ниже):
-I/Users/andreysaleba/projects/prebuilt-openssl/dist/openssl-1. 2d-ios/include

В поле Other Linker Flags добавляем следующие строки:

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

Все готово. Библиотека OpenSSL добавлена для обеих платформ.

Спасибо за просмотр!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть