Dart に実装中の Native Assets について

こんにちは、ぎもちんです。
こちらは [Flutter Advent Calendar 2023](https://qiita.com/advent-calendar/2023/flutter) 20日の記事です。

今回は Dart の FFI 周りを調べる中で気になった Native Assets という機能について紹介したいと思います。

# FFI について
Native Assets に入る前に、まず FFI について軽く触れておきます。

FFI とはざっくり説明すると、 Dart から他言語で記述された機能を使用するための仕組みです。
これを使うと、 Dart から C 言語等で記述されたネイティブの関数を呼び出すことができます。

### FFI で起きる問題
FFI を使った実装は便利な反面、 パッケージとして公開しづらいという問題があります。

ネイティブの機能を FFI で使うためには、ネイティブライブラリをリンクするケースが多いです。
ただ、この設定を自動で行える仕組みは Dart 自体にないため、パッケージ開発者や使用者が何かしら設定を行うことになります。

例外としてFlutter には `plugin_ffi`[^plugin_ffi] というプラグインテンプレートがあり、こちらを使うと自動で設定できます。
ただしこの場合、 Dart のパッケージとしては公開できず、あくまで Flutter のパッケージとなります。

[^plugin_ffi]:`plugin_ffi` テンプレートは Native Assets の導入に伴って段階的に削除されるようです [#131209](https://github.com/flutter/flutter/issues/131209)

# Native Assets について
上述の FFI が抱える問題を解決するため、 Dart パッケージで C のコードをビルドしたり、ネイティブライブラリを簡単に取り扱えるようにする機能が Native Assets となります。
[#129757](https://github.com/flutter/flutter/issues/129757) や [#50565](https://github.com/dart-lang/sdk/issues/50565) で主に話されていて、一部の機能は Dart 3.2 から使用できます。

Native Assets を用いるとソースコードのビルド処理や、ネイティブライブラリのダウンロード処理といったセットアップ処理を Dart のパッケージの一部として配布できるようになります。

※執筆時点では experimental な機能のため、今後変更が加わる可能性があります

## 試してみる
Native Assets を実際に試してみましょう！

:::note warn
以下の一部手順では Flutter の master バージョンが必要になります
:::

まだ experimental な機能のため、以下で有効化します
```console
flutter config --enable-native-assets
```

その後、以下のコマンドでパッケージを作成します
```console
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 アプリを実行してみると、こんな風に足し算の結果が表示されるはずです
![スクリーンショット 2023-12-19 0.21.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240790/4af56b9f-217f-d78c-d27a-f01ba5751c87.png)

:::note
sum(1, 2) = 1003 となっていますが、 C の実装はこのようになっているためデバッグモードでビルドされたことがわかります。
```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` を見てみると、以下のコードが書かれています。
```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
[ソースコード](https://github.com/dart-lang/native/blob/main/pkgs/native_assets_cli/lib/src/model/build_config.dart)
`BuildConfig` はその名の通り、ビルド設定を保持するクラスです。

ビルド成果物の出力先や、ビルド先の ABI や CPU アーキテクチャ、OS固有の設定値(iOS SDK や Android SDK バージョン等)を保持しています。

`BuildConfig.fromArgs` で初期化することができます。

### BuildOutput
[ソースコード](https://github.com/dart-lang/native/blob/main/pkgs/native_assets_cli/lib/src/model/build_output.dart)
`BuildOutput` はビルド成果物とその情報を書き込むためのクラスです。

このインスタンスに対して、ビルドしたネイティブライブラリを `Asset` という形で渡すと必要なリンク処理がのちに行われます。
また、 `build_output.yaml` というファイルにビルド成果物情報を書き込む処理も行なっています。

### CBuilder
[ソースコード](https://github.com/dart-lang/native/blob/main/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart)
`CBuilder` は C 言語のソースコードをビルドするためのクラスです。

こちらは Native Assets の拡張機能で、 `native_toolchain_c` というパッケージの機能となっています。
コンパイラの検索や、 `clang` や `cl` といったコンパイラの設定、コンパイル時定数の設定といったことを、ビルド対象のOSやアーキテクチャをもとに自動で行います。

`run` メソッドを呼び出すと、実際に C ソースファイルをコンパイルして `BuildOutput` へと出力します。

### `build.dart` がやっていること
以上を踏まえて、結局 `build.dart` が何をやってるのかコメントしてみるとこんな風になります
```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-12-20 1.05.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/240790/a7e81cc8-3680-25ac-a93f-5e9d7377c02d.png)

### 追記: 2023/01/29
[こちらの PR](https://github.com/flutter/flutter/pull/140907) で iOS でも dylib がリンクされるという不具合が解消されました。
今後は iOS と macOS では `Framework` が、 Android では `dylib` が生成されるようになります。


## 最後に
少し宣伝も兼ねますが、筆者の作成している [coast_audio](https://github.com/SKKbySSK/coast_audio) というライブラリがこの問題を抱えていて見つけた新機能でした。

事前にビルド済みのライブラリを `build.dart` で使うこともできたりして、 Dart や Flutter パッケージの幅が広がる嬉しい機能に思います。
正式リリースが待ち遠しいですね。

それではみなさま、良き Flutter ライフを！