こんにちは、FanTech本部Fanbase事業部に所属しています、瀬藤と申します!

今回はFanbase事業部でリリースした施策において検証したDart FFIを利用した実装とその実用例についてご紹介したいと思います!

そもそも Dart FFI とは

Dart FFI(Foreign Function Interface)とは、DartからCのインターフェースを持つ外部コード(ネイティブコード)を呼び出すための仕組みです。
C言語に限らず、C ABI(Application Binary Interface)に準拠した実装であれば、Dart側から直接その処理を呼び出すことができます。

呼び出し対象のコードは、静的ライブラリまたは動的ライブラリなどにしてアプリに組み込むことで実行可能になります。

Dart FFIを検証した背景

ある施策において、Webとアプリの両方で共通の動画関連処理を実行したい場面がありました。
ただし、これをサーバー側で処理するとサーバー負荷が高くなる懸念があったため、サーバー側で既に Go 言語で実装済みの処理を、各ユーザーの端末上で直接実行する方法を検証することになりました。

Web 側では WebAssembly(Wasm)として処理を移植し、アプリ側ではライブラリ化してアプリに組み込み、Dart FFI経由で呼び出す方針を試験的に検証しました。

最終的には別の事情によりこの方針は変更となりましたが、そこで得られた知見をこの記事で紹介したいと思います!

Go言語側のサンプルコード

今回はアプリからGo言語で実装した処理を呼び出す簡単な例として、2つの数値を受け取って加算した結果を返す関数をGo側に用意し、それをDart側から呼び出す構成を紹介します。

以下が Go言語側の実装です:

package main

import "C"

//export Add
func Add(a, b C.int) C.int {
    return a + b
}

func main() {}

冒頭でも書きましたがDartから呼び出すには、Go言語の関数を C ABIに準拠させる必要があります。
C ABIに準拠する上で必要な主なポイントはここでは以下2つです。

  • cgoを利用するように、’import “C”‘を宣言する
  • C から呼び出せるように、Add関数に’//export Add’を宣言する

Goのコードを iOS/Android向けにライブラリ化する

上記の Go言語のコードを cgo + Goビルドシステムを用いて、iOS用 .aファイル(静的ライブラリ)Android用 .soファイル(共有ライブラリ) に変換していきます。

iOS向けのビルド例(Simulator arm64)

CGO_ENABLED=1 \
CC=$(xcrun --sdk iphonesimulator --find clang) \
CGO_CFLAGS="-target arm64-apple-ios14.0-simulator -isysroot $(xcrun --sdk iphonesimulator --show-sdk-path)" \
GOARCH=arm64 \
GOOS=ios \
go build -o libmain.a -buildmode=c-archive main.go

⚠️ 実機対応(arm64)や Intelシミュレータ(x86_64)も考慮した実際のユースケースでは、各アーキテクチャ用.aを生成した上でxcframeworkにまとめるなどする必要があります。

Android向けのビルド例(arm64)

CGO_ENABLED=1 \
CC=$ANDROID_CLANG_PATH \
GOARCH=arm64 \
GOOS=android \
go build -buildmode=c-shared -o libmain.so main.go

⚠️ Androidも本来はarmeabi-v7a, x86_64, arm64-v8aなど複数アーキテクチャ用の.soを作成し、それぞれjniLibsに配置する必要がありますが、本記事ではarm64のみを扱います。

iOS/Androidそれぞれにライブラリを組み込む

iOS側の組み込み例

生成したlibmain.a(静的ライブラリ)とlibmain.h(ヘッダファイル)を、Flutterプロジェクトのios/Runner/ディレクトリに配置します。

Xcodeプロジェクト側で以下を行います:

  • libmain.aを [Link Binary With Libraries]に追加
  • libmain.hRunner-Bridging-Header.h#includeする
  • Other Linker Flags-force_load $(SRCROOT)/Runner/libmain.aを追加(リンカが最適化で関数を無視しないように)

Runner配下に.aとヘッダーファイルをそれぞれ配置

Android側の組み込み例

生成したlibmain.soをFlutterプロジェクトの以下のようなパスに配置します:

android/app/src/main/jniLibs/arm64-v8a/libmain.so

他のアーキテクチャに対応する場合はarmeabi-v7a/, x86_64/等も作成します。

jniLibs配下にそれぞれのアーキテクチャごとのsoファイルを配置

Dart FFI経由でGoの関数を呼び出す

ライブラリの組み込みができたら、Dart側からその関数を呼び出します。
まずはネイティブライブラリをロードします:

final DynamicLibrary nativeLib = Platform.isAndroid
    ? DynamicLibrary.open("libmain.so")
    : DynamicLibrary.process();

次に、シンボル名(関数名)で該当関数を検索し、型情報を定義して呼び出します:

typedef AddFunc = Int32 Function(Int32 a, Int32 b);
typedef Add = int Function(int a, int b);

final Add add = nativeLib
    .lookup<NativeFunction<AddFunc>>('Add')
    .asFunction<Add>();

final result = add(1, 2); // 結果は 3

このようにDart FFIを利用することで、FlutterアプリからGo言語で書かれた処理を呼び出すことが可能になりました!

実際の活用例や注意点

実際の活用例ですが、例えば以下のようなユースケースが考えられると思います!

  • 動画処理・画像処理などの低レベル処理において、実績のあるCライブラリをアプリに組み込みたい場合
  • アプリ・Webなど複数のプラットフォームで共通化した処理を組み込みたい場合

特に低レベル処理では、C/C++ライブラリがリアルタイム処理や大規模データで有利な場合があるので、検討する価値はあるかと思います!

また実際に活用する上では、以下の点に関する考慮も必要になるかと思います。

  • メモリ管理:ネイティブコード側で確保したメモリは、適切に解放する処理も入れる必要があります
  • エラーハンドリング:ネイティブコード側で発生したエラーをアプリ側で適切に扱うためには、戻り値でエラーコードを返すなどハンドリングの仕組みを実装する必要があります。また、exitabortのような呼び出しを行うと、アプリが強制終了する可能性がある点にも注意が必要です。

まとめ

この記事では、Go言語で実装された処理をFlutterアプリに組み込みDart FFI経由で呼び出す方法を紹介しました!

要点の整理

  • Go言語側の処理をC ABIに準拠した形で公開すれば、Dart FFI から呼び出せる
  • cgoを使ってGo言語のコードを静的ライブラリ(iOS)や共有ライブラリ(Android)にビルドできる
  • Dart 側では DynamicLibrary を通じて対象関数のシンボルをロードし、処理を実行することができる
  • ネイティブコード側の処理によってはメモリ管理やエラーハンドリングの考慮も必要になる

ユースケース自体はニッチかもしれませんが、FFIの柔軟さを活かせば他のC ABI準拠言語(Rustなど)にも応用できます。
使ってみると非常に面白い技術なので、ぜひ試してみてください!

参考資料