Интеграция React Native и C++ для iOS и Android

в 18:41, , рубрики: android, c++, iOS, react native, мобильная разработка, разработка мобильных приложений, Разработка под android, разработка под iOS

Недавно мне предложили поработать над одним интересным проектом. Требовалось разработать мобильное приложение для американского стартапа на платформах 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-эмулятора (кликабельна):

Интеграция React Native и C++ для iOS и Android - 1

Теперь рассмотрим, как работают 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 (картинки кликабельны):

Интеграция React Native и C++ для iOS и Android - 2

Интеграция React Native и C++ для iOS и Android - 3

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

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

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

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

Интеграция React Native и C++ для iOS и Android - 4

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

Интеграция React Native и C++ для iOS и Android - 5

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

Последний шаг – изменить код файла 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. (картинка кликабельна)

Интеграция React Native и C++ для iOS и Android - 6

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

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

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

Автор: Андрей Салеба

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js