Интеграция 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<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.

Сначала добавим ссылки на используемый в проекте 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<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.

Открываем 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 добавлена для обеих платформ.

Спасибо за просмотр!
  • +18
  • 3,2k
  • 2
Поделиться публикацией

Комментарии 2

    +1
    abiFilters "armeabi-v7a", "x86"
    Гугл требует, чтобы приложения переходили на 64 бита и вскоре не будет 32bit-only пропускать в маркет (как это уже провернул с min sdk). Думаю, стоит уже учитывать.
      0
      А можно было вместо React Native использовать Qt/QML. Вся эта «негомогенность» удручает.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое