LoginSignup
4
2

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

Last updated at Posted at 2023-12-19

こんにちは、ぎもちんです。
こちらは Flutter Advent Calendar 2023 20日の記事です。

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

FFI について

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

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

FFI で起きる問題

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

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

例外としてFlutter には plugin_ffi1 というプラグインテンプレートがあり、こちらを使うと自動で設定できます。
ただしこの場合、 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 アプリを実行してみると、こんな風に足し算の結果が表示されるはずです
スクリーンショット 2023-12-19 0.21.11.png

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 を見てみると、以下のコードが書かれています。

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 というパッケージの機能となっています。
コンパイラの検索や、 clangcl といったコンパイラの設定、コンパイル時定数の設定といったことを、ビルド対象のOSやアーキテクチャをもとに自動で行います。

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

build.dart がやっていること

以上を踏まえて、結局 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.dylibbuild_output.yaml といったファイルが書き込まれていると思います。

スクリーンショット 2023-12-20 1.05.50.png

追記: 2023/01/29

こちらの PR で iOS でも dylib がリンクされるという不具合が解消されました。
今後は iOS と macOS では Framework が、 Android では dylib が生成されるようになります。

最後に

少し宣伝も兼ねますが、筆者の作成している coast_audio というライブラリがこの問題を抱えていて見つけた新機能でした。

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

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

  1. plugin_ffi テンプレートは Native Assets の導入に伴って段階的に削除されるようです #131209

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2