React-Nativeを使ってiOSアプリを作ってみる(その3 C++でモジュール作成)

# はじめに

[React-Nativeを使ってiOSアプリを作ってみる(その1 環境構築)](https://qiita.com/rsm223_rip/items/f70c9c8939a773398b87)
[React-Nativeを使ってiOSアプリを作ってみる(その2 画面遷移)](https://qiita.com/rsm223_rip/items/402e781c94a5d33d9bfc)
👆これの続きです。

[Why a New Architecture
(React Native(公式)](https://reactnative.dev/docs/the-new-architecture/why)

経緯はよくわからんが公式サイトによると、
`React-Native`の`Ver0.68`から`New Architecture`になったらしい。
元々のReact-Nativeでは`Bridge`というコンポーネントを使って`JS layer`と`native layer`間のデータのやり取りを非同期でシリアライズ/デシリアライズをしていて不必要に待たされるだとか余分なオーバヘッドが発生していた。。。

で、Ver0.68から`New Acrhitecture`で`Bridge`から`JSI(JavaScript Interface)`を採用したことで、C++を呼べるようにして、
不必要に非同期で動いていた箇所を同期処理にしたり、シリアライズ,デシリアライズのオーバーヘッドを無くしたりしたそうです。
また、C++を導入したことで、プラットフォーム間に依存しない部分を抽象化して共有する狙いが有るみたいです。

ということで、公式サイトの手順に沿ってC++の`Native Module`の作り方を書いてこうと思います。


手順は大まかに以下の4ステップです。
※ Androidの場合、ちょっとやり方が変わります。

1. プロジェクトを作成する
2. JavaScript用の定義作成
3. cocoapods用にPodSpecファイル作成
4. アプリで使用するために、モジュールを登録
5. モジュールを作成
6. アプリで作成したモジュールを実行する

# 1. まず手始めに、手順の確認とモジュールを追加するプロジェクトを作成する

```bash
npx react-native init CxxTurboModulesGuide
cd CxxTurboModulesGuide

# ios フォルダで以下を実行(作りながら何回かやるし、今やらんでもいいかも)
cd ./ios
# 
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
cd ../

# C++などのファイルの格納先
# 内部的にパスが合ってればいいので、プロジェクト直下じゃなくてもフォルダ名がtmじゃなくてもOK
mkdir tm

# フォルダ構成はこんな感じになっているはず。
tree ../CxxTurboModulesGuide  -d -L 1 
../CxxTurboModulesGuide
├── __tests__
├── android
├── ios
├── node_modules
├── tm
└── vendor

7 directories
```

# 2.JavaScript用の定義作成

tm フォルダで以下のファイルを作成する。
`touch ./ios/NativeSampleModule.tx`

内容は以下の感じで、ここで定義したのが作成したアプリのインターフェースになるみたい。

```tsx:./tm/NativeSampleModule.ts
import type {TurboModule} from 'react-native/Libraries/TurboModule/RCTExport';
// import type {TurboModule} from 'react-native'; in future versions
import {TurboModuleRegistry} from 'react-native';

export interface Spec extends TurboModule {
  readonly reverseString: (input: string) => string;
}

export default TurboModuleRegistry.getEnforcing<Spec>(
  'NativeSampleModule',
);
```

# 3. cocoapods用にPodSpecファイル作成

## 3.1 package.json更新

package.jsonに以下を追加する。
`jsSrcsDir`はパスが合ってればOK

```diff_json:./package.json
{
  // ...
  "codegenConfig": {
    "name": "AppSpecs",
    "type": "all",
    "jsSrcsDir": "tm",
    "android": {
      "javaPackageName": "com.facebook.fbreact.specs"
    }
  }
}
```

## 3.2 AppTurboModules.podspecファイルを作成

tmフォルダにAppTurboModules.podspecファイルを作成する。

`touch ./tm/AppTurboModules.podspec`

内容は以下、とりあえず適当に書いた。(動作確認済み)
ここにビルドに必要な情報を書くっぽい(CMakefileみたいな感じ？)

```podspec:./tm/AppTurboModules.podspec
require "json"

Pod::Spec.new do |s|
  s.name            = "AppTurboModules"
  s.version         = "0.0.1"
  s.summary         = "description"
  # descriptionはなくてもいいみたい。
  #s.description     = package["description"]
  s.homepage        = "http://localhost/"
  s.license         = "MIT"
  s.platforms       = { :ios => "12.4" }
  s.author          = "Anonymous"
  s.source          = { :git => "", :tag => "#" }
  s.source_files    = "**/*.{h,cpp}"
  s.pod_target_xcconfig = {
    "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
  }
  install_modules_dependencies(s)
end
```

## 3.3 ios/Podfileを更新する

use_react_native()の直下に以下を追加

```diff_apple_script:./ios/Podfile
  use_react_native!(
    :path => config[:reactNativePath],
    # Hermes is now enabled by default. Disable by setting this flag to false.
    :hermes_enabled => flags[:hermes_enabled],
    :fabric_enabled => flags[:fabric_enabled],
    # Enables Flipper.
    #
    # Note that if you have use_frameworks! enabled, Flipper will not work and
    # you should disable the next line.
    :flipper_configuration => flipper_config,
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

+  if ENV['RCT_NEW_ARCH_ENABLED'] == '1'
+    # パスが合ってればOK
+    pod 'AppTurboModules', :path => "./tm"
+  end
```

# 4. アプリで使用するために、モジュールを登録

`ios/CxxTurboModulesGuide/AppDelegate.mm`に以下を追加する。
(Objective-Cの関数定義ってなんかクセが強いですね、、)
**別にObjective-Cでクラスを返す関数を作らなくても、C++側で実装して、Objective-Cから呼び出すこともできるっぽい**

```diff_objective_c:ios/CxxTurboModulesGuide/AppDelegate.mm
#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
+ #import <React/CoreModulesPlugins.h>
+ #import <ReactCommon/RCTTurboModuleManager.h>
+ #import <NativeSampleModule.h>

+ @interface AppDelegate () <RCTTurboModuleManagerDelegate> {}
+ @end

// ....

+ // ファイルの末尾あたりにでも追加しておけばOK
+ #pragma mark RCTTurboModuleManagerDelegate
+
+ // getTurboModule関数を追加
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(
+     const std::string &)name
+     jsInvoker:(std::shared_ptr<facebook::react::CallInvoker>)jsInvoker
+ {
+     // NativeSampleModuleが今回追加するモジュールのクラス
+     // NativeSampleModule.tsからも使われるっぽいので、一貫性のある書き方をすること。
+   if (name == "NativeSampleModule") {
+     return std::make_shared<facebook::react::NativeSampleModule>(jsInvoker);
+   }
+   return nullptr;
+ }
```

ファイルを更新したら一旦、以下を実行。
CxxTurboModulesGuide/ios/build/generated/iosに
`AppSpecsJSI.h` と `AppSpecsJSI-generated.cpp`が作成される。
`AppSpec`はプレフィックスで`package.json`の`codegenConfig.name`の値らしい

```bash
cd ios
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
cd ../

# AppSpecsJSI.h` と `AppSpecsJSI-generated.cpp`ができているはず。
ls ./ios/build/generated/ios
```

# 5. モジュールを作成

hファイルとcppファイルを作成する

> touch ./tm/NativeSampleModule.h
> touch ./tm/NativeSampleModule.cpp

```cpp:./tm/NativeSampleModule.h
#pragma once

#include <React-Codegen/AppSpecsJSI.h>
#include <memory>
#include <string>

namespace facebook::react {

// NativeSampleModuleCxxSpecは AppSpecsJSI.hで定義されてるクラス
class NativeSampleModule : public NativeSampleModuleCxxSpec<NativeSampleModule> {
 public:
  NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker);

  std::string reverseString(jsi::Runtime& rt, std::string input);
};

} // namespace facebook::react
```

```cpp:./tm/NativeSampleModule.cpp
#include "NativeSampleModule.h"

namespace facebook::react {

NativeSampleModule::NativeSampleModule(std::shared_ptr<CallInvoker> jsInvoker)
    : NativeSampleModuleCxxSpec(std::move(jsInvoker)) {}

// #1で定義したTypeScriptと同じ関数名にすること
std::string NativeSampleModule::reverseString(jsi::Runtime& rt, std::string input) {
  // 引数文字列を反転させて戻り値に返してるだけ
  return std::string(input.rbegin(), input.rend());
}

} // namespace facebook::react
```

.cpp/.hファイルを追加したら以下を実行

```bash
cd ./ios/
RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
cd ../
```

# 6. アプリで作成したモジュールを実行する

```diff_tsx:App.tsx
//...
import {
  Colors,
  DebugInstructions,
  Header,
  LearnMoreLinks,
  ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
// パスが合っていれば、どこでもいいはず。。
+ import NativeSampleModule from './tm/NativeSampleModule';
//...
        <View
          style={{
            backgroundColor: isDarkMode ? Colors.black : Colors.white,
          }}>
+         <Section title="Cxx TurboModule">
+          NativeSampleModule.reverseString(...) ={' '}
+          {NativeSampleModule.reverseString(
+            'the quick brown fox jumps over the lazy dog'
+          )}
+         </Section>
          <Section title="Step One">
            Edit <Text style={styles.highlight}>App.tsx</Text> to change this
            screen and then come back to see your edits.
          </Section>
//...
```

こんな感じで引数で渡した文字列が反転していればOKです。

![スクリーンショット 2023-08-21 18.42.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/276495/b898b8a3-c393-7dd0-3457-82c6d978ee4d.png)


# 最後に

個人的にはAndroidだからJavaとか,iOSだからSwift/Objective-cとか環境に合わせて使い分けるのめんどくさいし、ReactとC++でまとめてくれると嬉しいなと思いました。

### 追伸
`C++ Turbo Native Modules`の最後の方で、
こんな一文👇を見つけた。

> Calling OS specific APIs
> You can still call OS specific functions in the compilation unit (e.g., NS/CF APIs on Apple or Win32/WinRT APIs on Windows) as long as the method signatures only use std:: or jsi:: types.
> 
> For Apple specific APIs you need to change the extension of your implementation file from .cpp to .mm to be able to consume NS/CF APIs.

std::, jsi::をメソッドシグネチャに使っていれば、
OS固有のAPIが使えるっぽいけど`macOS`のAPIを`iOS`でも使えるということで合ってる？
おしえて、えらい人!