[Из песочницы] Интеграция React Native и C++ для iOS и Android
Недавно мне предложили поработать над одним интересным проектом. Требовалось разработать мобильное приложение для американского стартапа на платформах iOS и Android с помощью React Native. Ключевой технической особенностью и фактором, который однозначно решил мое участие в проекте, стала задача интегрировать библиотеку, написанную на языке С++. Для меня это могло быть новым опытом и новым профессиональным испытанием.
Почему было необходимо интегрировать С++ библиотеку
Данное приложение было необходимо для двухфакторной аутентификации с помощью протоколов FIDO UAF и U2F, использующих биометрические данные, таких как Face ID и Touch ID, и аналогичных технологий для Android платформы. Клиент для аутентификации был уже готов. Это была библиотека, написанная на С++ и применяемая некоторыми другими клиентами помимо мобильного приложения. Так что от меня требовалось встроить ее аналогичным образом в мобильное приложение на React Native.
Как я это делал
Существует подход для интеграции С++ в React Native приложение от Facebook. Однако проблема в том, что он работает только для платформы iOS, и не понятно, что делать с Android в данном случае. Мне же хотелось решить проблему сразу для двух платформ.
Форк инструмента Djinni от Dropbox, который позволяет генерировать кросс-платформенные объявления типов. По сути он является простым мобильным приложением на React Native с настроенной связью с Djinni. Именно его я взял за основу.
Для удобства код приложения разбит на два git-репозитория. В первом хранится исходный код React Native приложения, а во втором — Djinni и необходимые зависимости.
Дальнейшие шаги
Сначала необходимо объявить интерфейс взаимодействия С++ и React Native кода. В Djinni это делается с помощью .idl файлов. Откроем файл react-native-cpp-support/idl/main.Djinni в проекте и ознакомимся с его структурой.
В проекте для нашего удобства уже объявлены некоторые типы данных JavaScript и биндинги для них. Таким образом, мы можем работать с типами String, Array, Map, Promise и другими без какого-либо дополнительного их описания.
В примере этот файл выглядит так:
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 { 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));
}
Теперь понятно, как работают тестовые функции на стороне С++ и 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.
Еще одним важным шагом является добавление DjinniModulesPackage в наш проект. Для этого в файле react-native-cpp/android/app/src/main/java/com/rncpp/jni/DjinniModulesPackage.java укажем:
...
import com.rncpp.jni.DjinniModulesPackage;
...
public class MainApplication extends Application implements ReactApplication {
...
@Override
protected List getPackages() {
return Arrays.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 createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); }
@Override
public List createNativeModules(ReactApplicationContext reactContext) {
List 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.
Сначала добавим ссылки на используемый в проекте Objective-C и С++ код из support library. Для этого перенесем содержимое папок react-native-cpp-support/support-lib/objc/ и react-native-cpp-support/support-lib/cpp/ в XCode проект. В результате в дереве структуры проекта будут отображены папки с кодом support library (картинки кликабельны):
Таким образом, мы добавили описания JavaScript типов из support library в проект.
Следующий шаг — добавление сгенерированных objective-c оберток для нашего тестового С++ модуля. Нам потребуется перенести в проект код из папки react-native-cpp/ios/rncpp/Generated/.
Осталось добавить С++ код нашего модуля, для чего перенесем в проект код из папок 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 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
...
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.
Открываем iOS проект в XCode и в настройках в табе Build Settings добавляем путь к библиотеке openssl в поле Other C Flags (пример пути на моем компьютере ниже):
-I/Users/andreysaleba/projects/prebuilt-openssl/dist/openssl-1.0.2d-ios/include
В поле Other Linker Flags добавляем следующие строки:
-L/Users/andreysaleba/projects/prebuilt-openssl/dist/openssl-1.0.2d-ios/lib
-lcrypto
-lssl
Все готово. Библиотека OpenSSL добавлена для обеих платформ.
Спасибо за просмотр!