こんにちは、ぎもちんです。
こちらは Flutter Advent Calendar 2023 20日の記事です。
今回は Dart の FFI 周りを調べる中で気になった Native Assets という機能について紹介したいと思います。
FFI について
Native Assets に入る前に、まず FFI について軽く触れておきます。
FFI とはざっくり説明すると、 Dart から他言語で記述された機能を使用するための仕組みです。
これを使うと、 Dart から C 言語等で記述されたネイティブの関数を呼び出すことができます。
FFI で起きる問題
FFI を使った実装は便利な反面、 パッケージとして公開しづらいという問題があります。
ネイティブの機能を FFI で使うためには、ネイティブライブラリをリンクするケースが多いです。
ただ、この設定を自動で行える仕組みは Dart 自体にないため、パッケージ開発者や使用者が何かしら設定を行うことになります。
例外としてFlutter には plugin_ffi
1 というプラグインテンプレートがあり、こちらを使うと自動で設定できます。
ただしこの場合、 Dart のパッケージとしては公開できず、あくまで Flutter のパッケージとなります。
Native Assets について
上述の FFI が抱える問題を解決するため、 Dart パッケージで C のコードをビルドしたり、ネイティブライブラリを簡単に取り扱えるようにする機能が Native Assets となります。
#129757 や #50565 で主に話されていて、一部の機能は Dart 3.2 から使用できます。
Native Assets を用いるとソースコードのビルド処理や、ネイティブライブラリのダウンロード処理といったセットアップ処理を Dart のパッケージの一部として配布できるようになります。
※執筆時点では experimental な機能のため、今後変更が加わる可能性があります
試してみる
Native Assets を実際に試してみましょう!
以下の一部手順では Flutter の master バージョンが必要になります
まだ experimental な機能のため、以下で有効化します
flutter config --enable-native-assets
その後、以下のコマンドでパッケージを作成します
flutter create --template=package_ffi example_package
なんとこれだけで完了です
以下のようなディレクトリが生成されたと思います。
example_package
┣ example
┃ ┗ ...
┣ lib
┃ ┣ example_package.dart
┃ ┗ example_package_bindings_generated.dart
┣ src
┃ ┣ example_package.c
┃ ┗ example_package.h
┣ build.dart
┗ ffigen.yaml
src/example_package.c
の中に C で足し算をする sum 関数が実装されていて、
example_package_bindings_generated.dart
に FFI を用いて sum 関数を呼ぶためのコードが記述されています。
試しに example アプリを実行してみると、こんな風に足し算の結果が表示されるはずです
sum(1, 2) = 1003 となっていますが、 C の実装はこのようになっているためデバッグモードでビルドされたことがわかります。
FFI_PLUGIN_EXPORT intptr_t sum(intptr_t a, intptr_t b) {
#ifdef DEBUG
return a + b + 1000;
#else
return a + b;
#endif
}
build.dart
を覗いてみる
ビルド処理が書かれている build.dart
を見てみると、以下のコードが書かれています。
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:logging/logging.dart';
import 'package:native_assets_cli/native_assets_cli.dart';
const packageName = 'example_package';
void main(List<String> args) async {
final buildConfig = await BuildConfig.fromArgs(args);
final buildOutput = BuildOutput();
final cbuilder = CBuilder.library(
name: packageName,
assetId: 'package:$packageName/${packageName}_bindings_generated.dart',
sources: [
'src/$packageName.c',
],
);
await cbuilder.run(
buildConfig: buildConfig,
buildOutput: buildOutput,
logger: Logger('')..onRecord.listen((record) => print(record.message)),
);
await buildOutput.writeToFile(outDir: buildConfig.outDir);
}
なんとなく、src/$packageName.c
をビルドして書き込んでるな〜というのが見て取れるかなと思います。
このコードをより具体的に読んでみると、
- BuildConfig
- BuildOutput
- CBuilder
という、おおよそ3人の登場人物がいるとわかります。
それぞれ何をしているかみてみましょう
BuildConfig
ソースコード
BuildConfig
はその名の通り、ビルド設定を保持するクラスです。
ビルド成果物の出力先や、ビルド先の ABI や CPU アーキテクチャ、OS固有の設定値(iOS SDK や Android SDK バージョン等)を保持しています。
BuildConfig.fromArgs
で初期化することができます。
BuildOutput
ソースコード
BuildOutput
はビルド成果物とその情報を書き込むためのクラスです。
このインスタンスに対して、ビルドしたネイティブライブラリを Asset
という形で渡すと必要なリンク処理がのちに行われます。
また、 build_output.yaml
というファイルにビルド成果物情報を書き込む処理も行なっています。
CBuilder
ソースコード
CBuilder
は C 言語のソースコードをビルドするためのクラスです。
こちらは Native Assets の拡張機能で、 native_toolchain_c
というパッケージの機能となっています。
コンパイラの検索や、 clang
や cl
といったコンパイラの設定、コンパイル時定数の設定といったことを、ビルド対象のOSやアーキテクチャをもとに自動で行います。
run
メソッドを呼び出すと、実際に C ソースファイルをコンパイルして BuildOutput
へと出力します。
build.dart
がやっていること
以上を踏まえて、結局 build.dart
が何をやってるのかコメントしてみるとこんな風になります
const packageName = 'example_package';
void main(List<String> args) async {
// args からビルド設定を読み込み
final buildConfig = await BuildConfig.fromArgs(args);
final buildOutput = BuildOutput();
// ビルドしたいソースコードを指定して、CBuilderを初期化
final cbuilder = CBuilder.library(
name: packageName,
assetId: 'package:$packageName/${packageName}_bindings_generated.dart',
sources: [
'src/$packageName.c',
],
);
// ソースコードをビルドして BuildOutput へ出力
await cbuilder.run(
buildConfig: buildConfig,
buildOutput: buildOutput,
logger: Logger('')..onRecord.listen((record) => print(record.message)),
);
// ビルド成果物の情報を build_output.yaml へ出力
await buildOutput.writeToFile(outDir: buildConfig.outDir);
}
ちなみにビルド成果物は、 .dart_tool/natvei_assets_builder/<ランダムな名前>/out
ディレクトリ内に書き込まれるようです。
example
アプリをビルドすると libexample_package.dylib
や build_output.yaml
といったファイルが書き込まれていると思います。
追記: 2023/01/29
こちらの PR で iOS でも dylib がリンクされるという不具合が解消されました。
今後は iOS と macOS では Framework
が、 Android では dylib
が生成されるようになります。
最後に
少し宣伝も兼ねますが、筆者の作成している coast_audio というライブラリがこの問題を抱えていて見つけた新機能でした。
事前にビルド済みのライブラリを build.dart
で使うこともできたりして、 Dart や Flutter パッケージの幅が広がる嬉しい機能に思います。
正式リリースが待ち遠しいですね。
それではみなさま、良き Flutter ライフを!