【Flutter2対応】dart:ffi を Flutter で使ってみる

dart:ffi をしこたま触る必要があったのでまとめてみました

本記事では分かりやすさを優先した方法にしています
公式な実装の仕方は Flutter のドキュメントをご参照ください ([Binding to native code using dart:ffi](https://flutter.dev/docs/development/platform-integration/c-interop))

# dart:ffi とは
* Dart foreign function interface の略です
* 他の言語で実装された関数(function)を呼び出す事が出来ます
  * 現在では実質C言語とC++のみですが、C言語を間に挟む事で利用可能な言語もあります
* 具体的にはダイナミックリンクライブラリ (拡張子 .dll .so .dylib のファイル)を読み込み、その中の関数を実行します
* Flutter の MethodChannel と出来る事がやや近いです
  * MethodChannel より手間がかかりますが、状況によってはメリットもあります

### メリット
* MethodChannel と違い `async` `await` が不要です
* 単体テスト(`flutter test`)も出来ます
  * Flutter2でデスクトップアプリがサポートされたので、将来的には MethodChannel でも実行出来る様になると思われます
* FlutterではなくDart単体で動作します。なのでサーバーサイドDartでもそのままコードを利用出来ます

### デメリット
* C言語のポインタをある程度理解する必要があります
* コンパイルをして、各プラットフォームごとにダイナミックリンクライブラリを作成する必要があります

# 実装
単純に2つの整数を足して返すだけの `sum` 関数を作って呼び出してみます。
前述の通り、各プラットフォームごとにダイナミックリンクライブラリを作成する必要があります

|OS|拡張子|
|-----------|------------|
|iOS|`.framework``.dylib`|
|macOS|`.framework``.dylib`|
|Android|`.so`|
|Windows|`.dll`|

## Dart (Flutter)

``` sum.dart
import 'dart:ffi';
import 'dart:io';

int sum(int num1, int num2) {
  var libraryPath = "";
  if (Platform.isIOS)
    libraryPath = "sum.framework/sum";
  else if (Platform.isMacOS)
    libraryPath = "sum.framework/sum";
  else if (Platform.isAndroid)
    libraryPath = "libsum.so";
  else if (Platform.isWindows) libraryPath = "sum.dll";

  if (libraryPath.isEmpty) {
    assert(false, "Dynamic library is not compatible with this platform.");
    return 0;
  }

  final dylib = DynamicLibrary.open(libraryPath);

  // 呼び出す関数を検索してポインタを取得
  final pointer =
      dylib.lookup<NativeFunction<Int32 Function(Int32, Int32)>>('sum');
  // ポインタを Dart で呼び出せる関数として定義
  final sum = pointer.asFunction<int Function(int, int)>();

  return sum(num1, num2);
}
```

上記のコードは以下の流れとなっています

1. ダイナミックリンクライブラリを開く[^1]
2. sum関数を検索してポインタとして取得
3. sum関数を Dart で呼び出せる関数に変換
4. sum関数の呼び出し

後は各プラットフォームごとにダイナミックリンクライブラリを作成していきます

## ライブラリ作成
### C言語（プラットフォーム共通）
``` sum.h
extern int sum(int, int);
```

``` sum.c
#include "sum.h"

int sum(int num1, int num2) {
    return num1 + num2;
}
```

### iOS, macOS
`sum.framework`を、iOS用とmacOS用でそれぞれ作成します

Xcode で Framework プロジェクトを作成します。
以下の画像は iOS のものですが、macOS用の場合は上のタブを macOS に切り替えて Framework を選択してください
![iosmacos-ss0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/56eb2daf-e0c6-0e76-5d6f-95088bdcf95c.png)

先ほどの `sum.h` と `sum.c` をプロジェクトに追加してビルドします
![iosmacos-ss1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/731d2ae7-6412-5312-f8ed-35444f10f9bd.png)

`sum.framework` (上記の `Products` にある物) を、
利用したい Flutter プロジェクトの ios ディレクトリまたは macos ディレクトリ以下にコピーしてください

Flutter プロジェクトの `Runner.xcworkspace` の `Build Phases` にある `Link Binary With Libraries` と `Embed Frameworks` (macOS の場合は `Bundle Framework`) に `sum.framework` を追加します

![iosmacos-ss2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/712f14eb-8b4a-12ac-8583-d4eac377c66d.png)

iOSでの注意点としては、一つの`sum.framework` は実機かシミュレーターのどちらかでしか動作しません

`sum.framework`のビルド時に `Product`→`Destination`→`Any iOS Device` をターゲットにすると実機でのみ動作しますし、
`iOS Simulators`のどれかをターゲットにするとシミュレーターでのみ動作するライブラリになります

実機とシミュレーター両方で動作させるには **XCFramework** を作成する必要があります。
参考: https://qiita.com/Arime/items/ee9a41d849b473181728

`flutter test` を macOS で動作させたい場合は `sum.framework` を
プロジェクトの一番上(`flutter`コマンドを実行する階層)にも配置します

Xcodeに追加した`sum.framework`(`macos`ディレクトリ以下に配置した物)を使いたい場合は、`sum.dart`の`libraryPath`を、テスト時のみ`macos/sum.framework/sum`となる様な形に処理を加えてください

### Android
公式では`ndk-build`スクリプトや`cmake`コマンドで作る方法([参考](https://developer.android.com/ndk/guides/build?hl=ja))が紹介されていますが、
今回は Android Studio で作ります

`Native C++` プロジェクトを作成します

![android-ss0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/5d56fdb4-57de-2fa6-44fd-a20a7206229a.png)

`app/src/main/cpp`ディレクトリに移動し、`native-lib.cpp` を削除して代わりに `sum.c` と `sum.h` を配置します

`CMakeLists.txt` を以下の内容に書き換えます

```CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)

project("sum")

add_library(sum SHARED sum.h sum.c)
```

`app/build.gradle` の `com.android.application` を `com.android.library` に変更し、`applicationId`の行を削除します

```app/build.gradle
plugins {
    id 'com.android.application' // id 'com.android.library' に変更
    id 'kotlin-android'
}

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.example.sum" // ←削除
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

// 以下略
```

この状態で `Build`→`Make Project`をすると、
`app/build/outputs/aar`ディレクトリの中に`app-debug.aar`というファイルが作成されます

![android-ss1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/ba723e68-de77-d6db-cc53-448dcaacd40f.png)

拡張子は`.aar`ですが、実際はただのzipファイルなので解凍ソフトで開けます。
開けない場合は拡張子を`.zip`にしてください。
また、何故かmacOSの標準アプリだと開けないので自分は [The Unarchiver](https://apps.apple.com/jp/app/the-unarchiver/id425424353?mt=12) で開きました

ズラッとディレクトリが並んでいます

![android-ss2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/10829836-b11b-0226-65da-471f81d9f72b.png)

中にある`jni`フォルダを`jniLibs`という名前に変えて、Flutterプロジェクトの
`android/app/src/main`に配置します

これで Android でも動作する様になります

ちなみに`Native C++` プロジェクトを作成した際に自動で作られるJavaやKotlinのファイルは不要なので、気になる場合は以下の手順で削除も出来ます

1. `app/src/main/java`、`app/src/main/res`、`app/src/test`、`app/src/androidTest` ディレクトリを削除
2. `app/src/main/AndroidManifest.xml` から `<application>〜</application>`の部分を削除
3. `build.gradle`（appディレクトリでは無い方）から、テスト関連の行を削除  
（`test`で検索して、ヒットした行を全て削除すればOKです）


### Windows
```
公式ドキュメントにはまだWindows版に関しての記載はない為
あくまで私的に動作を確認したものとなっております。
実使用の際にはご留意の上でお願いいたします。
```

Visual Studio 2019 をインストールします。無料のコミュニティ版で構いません。
インストール時にオプションで「**C++ CMake ツール**」を追加する様にします。
https://visualstudio.microsoft.com/ja/downloads/

次に、cmake でビルドをする為の準備をします。
Android と同じ様に、`CMakeLists.txt`と`sum.c`、`sum.h`を同じフォルダに配置します

加えて、**sum.def**を新しく作成します

```sum.def
EXPORTS
	sum
```

CMakeLists.txtを以下の内容にします。
Androidとほぼ同じ内容ですが、sum.defが追加されています

```CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)

project("sum")

add_library(sum SHARED sum.c sum.h sum.def)
```

スタートメニューより`x64 Native Tools Command Prompt for VS 2019`を起動します

![windows-ss0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679221/4757349c-afad-deed-8f68-162066af567c.png)

先ほどの CMakeLists.txt などがあるディレクトリまで移動した後、次のコマンドを実行します

```
mkdir build
cd build
cmake ..
```

成功すると、buildフォルダの中に Visual Studio のプロジェクトが作成されますので、「`sum.sln`」を開きます

ビルドターゲットを「**Release**」にして、`ビルド`→`ソリューションのビルド`を実行します

ビルドが成功すると、Releaseフォルダの中に`sum.dll`が作られています

`flutter build windows`で動作させたい場合は、生成された`exe`ファイルと同じ場所(`build¥windows¥runner¥Release`)にコピーします

`flutter run -d window`や`flutter test`で用いたい場合は、
`sum.dll`をプロジェクトの一番上(`flutter`コマンドを実行する階層)に配置します。
先ほどの Dart の`libraryPath`を任意のパスに変更すれば他のフォルダでも構いませんが、`flutter build windows`でも反映されてしまう為、リリースビルド時とで処理の変更をおすすめします

これで Windows でも動作する様になります

なお、今回の方法では 64bit 用のDLLを作成出来ます。
32bit の Windows で動作をさせたい場合は
`x86 Native Tools Command Prompt for VS 2019`
を使用します


# 備考
<details><summary>本記事は以下の環境で検証しました</summary><div>

```
Flutter 2.0.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision c5a4b4029c (4 days ago) • 2021-03-04 09:47:48 -0800
Engine • revision 40441def69
Tools • Dart 2.12.0

Android Studio 4.1.1
Build #AI-201.8743.12.41.6953283, built on November 5, 2020
Runtime version: 1.8.0_242-release-1644-b3-6915495 x86_64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.15.7
GC: ParNew, ConcurrentMarkSweep
Memory: 1237M
Cores: 8
Registry: ide.new.welcome.screen.force=true, external.system.auto.import.disabled=true
Non-Bundled Plugins: Dart, io.flutter
cmake version 3.18.1
NDK version 23.0.7123448 rc1

Xcode Version 12.4 (12D4e)

Microsoft Visual Studio Community 2019
Version 16.8.5
VisualStudio.16.Release/16.8.5+31005.135
Microsoft .NET Framework
Version 4.8.04084
インストールされているバージョン:Community
Visual C++ 2019   00435-60000-00000-AA087
Microsoft Visual C++ 2019
Visual Studio Tools for CMake   1.0
cmake version 3.18.20081302-MSVC_2
```
</div></details>

<details><summary>編集履歴</summary><div>

```
2021/03/09 Flutter 2.0.1 で再検証
           デスクトップアプリに対応
2021/03/03 公式ドキュメントへのリンクと`DynamicLibrary.open`に関する注釈を追加
2021/02/27 初公開
```
</div></details>

[^1]: iOSに対してはFlutterのドキュメントでは、 `DynamicLibrary.open` ではなく `DynamicLibrary.process` または `DynamicLibrary.execute` の使用を推奨しています。理由としてはAppleの審査が通るか不明だからとの事ですが、実際に`DynamicLibrary.open`を用いたアプリを申請し、問題なく通った為こちらの方法を記述しています。