package:ffigen を使用した Objective-C および Swift の相互運用
Dart Native プラットフォームの macOS または iOS 上で実行される Dart モバイル、コマンドライン、サーバーアプリは、dart:ffi と package:ffigen を使用して Objective-C および Swift API を呼び出すことができます。
dart:ffi を使用すると、Dart コードはネイティブ C API と対話できます。Objective-C は C をベースとしており、C と互換性があるため、dart:ffi のみを使用して Objective-C API と対話することは可能です。ただし、これを行うには多くの定型コードが必要になるため、package:ffigen を使用すると、指定された Objective-C API の Dart FFI バインディングを自動生成できます。FFI と C コードへの直接のインターフェイスについて詳しくは、C 相互運用ガイドをご覧ください。
Swift API 用の Objective-C ヘッダーを生成することで、dart:ffi と package:ffigen が Swift と対話できるようになります。
Objective-C の例
#このガイドでは、AVAudioPlayer のバインディングを生成するために package:ffigen を使用する例を説明します。この API は macOS SDK 10.7 以上が必要です。バージョンを確認し、必要に応じて Xcode を更新してください。
xcodebuild -showsdksObjective-C API をラップするためのバインディングの生成は、C API をラップするのと似ています。API を記述するヘッダーファイルに直接 package:ffigen を指定し、次に dart:ffi でライブラリをロードします。
package:ffigen は LLVM を使用して Objective-C ヘッダーファイルを解析するため、まず LLVM をインストールする必要があります。詳細については、ffigen README の LLVM のインストールを参照してください。
ffigen の設定
#まず、package:ffigen を開発依存関係として追加します。
dart pub add --dev ffigen次に、ffigen がヘッダーに含まれる Objective-C API のバインディングを生成するように設定します。ffigen の設定オプションは、pubspec.yaml ファイルのトップレベルの ffigen エントリに配置します。あるいは、ffigen の設定を独自の .yaml ファイルに配置することもできます。
ffigen:
name: AVFAudio
description: Bindings for AVFAudio.
language: objc
output: 'avf_audio_bindings.dart'
headers:
entry-points:
- '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h'name は生成されるネイティブライブラリのラッパークラスの名前であり、description はそのクラスのドキュメントで使用されます。output は ffigen が作成する Dart ファイルのパスです。エントリーポイントは、API を含むヘッダーファイルです。この例では、内部の AVAudioPlayer.h ヘッダーです。
pubspec.yaml の 例の設定を見ると、exclude と include オプションも重要であることがわかります。デフォルトでは、ffigen はヘッダー内で見つかったすべてのものと、それらのバインディングが依存する他のヘッダー内のすべてのものに対してバインディングを生成します。ほとんどの Objective-C ライブラリは Apple の内部ライブラリに依存しており、これらは非常に大きいです。フィルターなしでバインディングが生成されると、結果のファイルは数百万行になる可能性があります。この問題を解決するために、ffigen の設定には、関心のないすべての関数、構造体、列挙などをフィルターで除外できるフィールドがあります。この例では、AVAudioPlayer のみに興味があるため、それ以外はすべて除外できます。
exclude-all-by-default: true
objc-interfaces:
include:
- 'AVAudioPlayer'AVAudioPlayer がこのように明示的に含まれているため、ffigen は他のすべてのインターフェイスを除外します。exclude-all-by-default フラグは、ffigen にそれ以外すべてを除外するように指示します。その結果、AVAudioPlayer と、NSObject や NSString などの依存関係以外は何も含まれません。そのため、数百万行のバインディングの代わりに、数万行になります。
より詳細な制御が必要な場合は、exclude-all-by-default を使用する代わりに、すべての宣言を個別に除外または含めることができます。
functions:
exclude:
- '.*'
structs:
exclude:
- '.*'
unions:
exclude:
- '.*'
globals:
exclude:
- '.*'
macros:
exclude:
- '.*'
enums:
exclude:
- '.*'
unnamed-enums:
exclude:
- '.*'これらの exclude エントリはすべて、正規表現 '.*' を除外し、これは任意の文字列に一致します。
preamble オプションを使用して、生成されたファイルの上部にテキストを挿入することもできます。この例では、preamble を使用して、生成されたファイルの上部にリンター無視ルールを挿入しました。
preamble: |
// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field, return_of_invalid_type, void_checks, annotate_overrides, no_leading_underscores_for_local_identifiers, library_private_types_in_public_api設定オプションの完全なリストについては、ffigen readme を参照してください。
Dart バインディングの生成
#バインディングを生成するには、例のディレクトリに移動し、ffigen を実行します。
dart run ffigenこれにより、pubspec.yaml ファイル内のトップレベルの ffigen エントリが検索されます。ffigen の設定を別のファイルに配置した場合は、--config オプションを使用し、そのファイルを指定してください。
dart run ffigen --config my_ffigen_config.yamlこの例では、avf_audio_bindings.dart が生成されます。
このファイルには、AVFAudio というクラスが含まれています。これは、FFI を使用してすべての API 関数をロードし、それらを呼び出すための便利なラッパーメソッドを提供するネイティブライブラリラッパーです。このファイル内の他のクラスはすべて、AVAudioPlayer やその依存関係など、必要な Objective-C インターフェイスの Dart ラッパーです。
バインディングの使用
#これで、生成されたライブラリをロードして対話する準備ができました。例のアプリ play_audio.dart は、コマンドライン引数として渡されたオーディオファイルをロードして再生します。最初のステップは、dylib をロードし、ネイティブ AVFAudio ライブラリをインスタンス化することです。
import 'dart:ffi';
import 'avf_audio_bindings.dart';
const _dylibPath =
'/System/Library/Frameworks/AVFAudio.framework/Versions/Current/AVFAudio';
void main(List<String> args) async {
final lib = AVFAudio(DynamicLibrary.open(_dylibPath));内部ライブラリをロードしているため、dylib パスは内部フレームワーク dylib を指しています。独自の .dylib ファイルをロードすることもできます。あるいは、ライブラリがアプリに静的にリンクされている場合(iOS ではよくある)、DynamicLibrary.process() を使用することもできます。
final lib = AVFAudio(DynamicLibrary.process());例の目標は、コマンドライン引数で指定された各オーディオファイルを順番に再生することです。各引数について、まず Dart の String を Objective-C の NSString に変換する必要があります。生成された NSString ラッパーには、この変換を処理する便利なコンストラクターと、Dart の String に戻す toString() メソッドがあります。
for (final file in args) {
final fileStr = NSString(lib, file);
print('Loading $fileStr');オーディオプレーヤーは NSURL を期待するため、次に fileURLWithPath: メソッドを使用して NSString を NSURL に変換します。: は Dart のメソッド名では無効な文字であるため、バインディングでは _ に変換されています。
final fileUrl = NSURL.fileURLWithPath_(lib, fileStr);これで、AVAudioPlayer を構築できます。Objective-C オブジェクトの構築には 2 つの段階があります。alloc はオブジェクトのメモリを割り当てますが、初期化はしません。init* で始まる名前のメソッドは、これらの初期化を行います。一部のインターフェイスには、これら両方のステップを実行する new* メソッドも用意されています。
AVAudioPlayer を初期化するには、initWithContentsOfURL:error: メソッドを使用します。
final player =
AVAudioPlayer.alloc(lib).initWithContentsOfURL_error_(fileUrl, nullptr);Objective-C はメモリ管理に参照カウントを使用しますが(retain、release、およびその他の関数を介して)、Dart 側ではメモリ管理は自動的に処理されます。Dart ラッパーオブジェクトは Objective-C オブジェクトへの参照を保持し、Dart オブジェクトがガベージコレクションされると、生成されたコードは NativeFinalizer を使用してその参照を自動的に解放します。
次に、後でオーディオの終了を待つために必要となるオーディオファイルの長さを検索します。duration は @property(readonly) です。Objective-C のプロパティは、生成された Dart ラッパーオブジェクトのゲッターとセッターに変換されます。duration は readonly なので、ゲッターのみが生成されます。
結果の NSTimeInterval は単なる型エイリアス化された double なので、Dart の .ceil() メソッドを使用してすぐに次の秒に切り上げることができます。
final durationSeconds = player.duration.ceil();
print('$durationSeconds sec');最後に、play メソッドを使用してオーディオを再生し、ステータスを確認して、オーディオファイルの期間を待ちます。
final status = player.play();
if (status) {
print('Playing...');
await Future<void>.delayed(Duration(seconds: durationSeconds));
} else {
print('Failed to play audio.');
}コールバックとマルチスレッドの制限
#マルチスレッドの問題は、Objective-C 相互運用に対する Dart の実験的なサポートの最大の制限事項です。これらの制限は、Dart アイソレートと OS スレッドの関係、および Apple の API がマルチスレッドを処理する方法に起因します。
- Dart アイソレートはスレッドと同じではありません。アイソレートはスレッド上で実行されますが、特定の Сレッド上で実行されることは保証されておらず、VM は警告なしにアイソレートが実行されている Сレッドを変更する可能性があります。アイソレートを特定の Сレッドにピン留めできるようにする オープン機能リクエストがあります。
ffigenは Dart 関数を Objective-C ブロックに変換することをサポートしていますが、ほとんどの Apple API は、コールバックがどの Сレッドで実行されるかについて保証を行っていません。- UI 操作に関わるほとんどの API は、メイン Сレッド(Flutter ではプラットフォーム Сレッドとも呼ばれます)でのみ呼び出すことができます。
- 多くの Apple API はスレッドセーフではありません。
最初の 2 つの点は、1 つのアイソレートで作成されたコールバックが、別のアイソレートを実行している Сレッド、またはまったくアイソレートではない Сレッドで呼び出される可能性があることを意味します。使用しているコールバックの種類によっては、アプリがクラッシュする可能性があります。Pointer.fromFunction または NativeCallable.isolateLocal を使用して作成されたコールバックは、所有アイソレートの Сレッドで呼び出す必要があります。そうしないとクラッシュします。NativeCallable.listener を使用して作成されたコールバックは、どの Сレッドからでも安全に呼び出すことができます。
3 番目の点は、生成された Dart バインディングを使用して一部の Apple API を直接呼び出すことはスレッドセーフではない可能性があることを意味します。これにより、アプリがクラッシュしたり、その他の予期しない動作が発生したりする可能性があります。この制限を回避するには、メイン Сレッドに呼び出しをディスパッチする Objective-C コードを記述します。詳細については、Objective-C ディスパッチドキュメントを参照してください。
最後の点に関して、Dart アイソレートは Сレッドを切り替えることができますが、一度に 1 つの Сレッドでのみ実行されます。したがって、対話している API は、スレッドセーフではない場合でも、スレッドに敵対的ではなく、呼び出し元 Сレッドに関する制約がない限り、スレッドセーフである必要はありません。
これらの制限を念頭に置いておけば、Objective-C コードと安全に対話できます。
Swift の例
#この例では、Swift クラスを Objective-C と互換性のあるものにする方法、ラッパーヘッダーを生成する方法、および Dart コードからそれを呼び出す方法を示しています。
Objective-C ラッパーヘッダーの生成
#@objc アノテーションを使用することで、Swift API を Objective-C と互換性のあるものにできます。使用したいクラスやメソッドはすべて public にし、クラスが NSObject を継承していることを確認してください。
import Foundation
@objc public class SwiftClass: NSObject {
@objc public func sayHello() -> String {
return "Hello from Swift!";
}
@objc public var someField = 123;
}サードパーティライブラリと対話していて、そのコードを変更できない場合は、使用したいメソッドを公開する Objective-C 互換のラッパークラスを記述する必要がある場合があります。
Objective-C / Swift の相互運用について詳しくは、Swift ドキュメントを参照してください。
クラスを互換性のあるものにした後、Objective-C ラッパーヘッダーを生成できます。これは Xcode を使用するか、Swift コマンドラインコンパイラ swiftc を使用して行うことができます。この例ではコマンドラインを使用します。
swiftc -c swift_api.swift \
-module-name swift_module \
-emit-objc-header-path swift_api.h \
-emit-library -o libswiftapi.dylibこのコマンドは Swift ファイル swift_api.swift をコンパイルし、ラッパーヘッダー swift_api.h を生成します。また、後でロードする dylib libswiftapi.dylib も生成します。
ヘッダーを問題なく開いてインターフェイスが想定どおりであることを確認することで、生成が正しく行われたことを検証できます。ファイルの最下部付近に、次のようなものが表示されるはずです。
SWIFT_CLASS("_TtC12swift_module10SwiftClass")
@interface SwiftClass : NSObject
- (NSString * _Nonnull)sayHello SWIFT_WARN_UNUSED_RESULT;
@property (nonatomic) NSInteger someField;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@endインターフェイスが欠落しているか、すべてのメソッドが含まれていない場合は、それらがすべて @objc および public でアノテーションされていることを確認してください。
ffigen の設定
#Ffigen は Objective-C ラッパーヘッダー swift_api.h のみを認識します。そのため、この設定のほとんどは Objective-C の例と似ており、言語を objc に設定することも含まれます。
ffigen:
name: SwiftLibrary
description: Bindings for swift_api.
language: objc
output: 'swift_api_bindings.dart'
exclude-all-by-default: true
objc-interfaces:
include:
- 'SwiftClass'
module:
'SwiftClass': 'swift_module'
headers:
entry-points:
- 'swift_api.h'
preamble: |
// ignore_for_file: camel_case_types, non_constant_identifier_names, unused_element, unused_field, return_of_invalid_type, void_checks, annotate_overrides, no_leading_underscores_for_local_identifiers, library_private_types_in_public_api前述のように、言語を objc に設定し、エントリーポイントをヘッダーに設定します。デフォルトですべてを除外し、バインディングしているインターフェイスを明示的に含めます。
ラップされた Swift API の設定と純粋な Objective-C API の設定との間には、1 つの重要な違いがあります。それは、objc-interfaces -> module オプションです。swiftc がライブラリをコンパイルするとき、Objective-C インターフェイスにモジュールプレフィックスを付けます。内部的には、SwiftClass は swift_module.SwiftClass として登録されます。ffigen にこのプレフィックスを知らせる必要があるため、dylib から正しいクラスをロードできます。
すべてのクラスにこのプレフィックスが付いているわけではありません。たとえば、NSString や NSObject は内部クラスであるため、モジュールプレフィックスは付きません。そのため、module オプションはクラス名をモジュールプレフィックスにマップします。正規表現を使用して、複数のクラス名を一度に一致させることもできます。
モジュールプレフィックスは、-module-name フラグで swiftc に渡したものです。この例では swift_module です。このフラグを明示的に設定しない場合、デフォルトで Swift ファイルの名前が使用されます。
モジュール名がわからない場合は、生成された Objective-C ヘッダーを確認することもできます。@interface の上に、SWIFT_CLASS マクロがあります。
SWIFT_CLASS("_TtC12swift_module10SwiftClass")
@interface SwiftClass : NSObjectマクロ内の文字列は少しわかりにくいですが、モジュール名とクラス名が含まれていることがわかります: "_TtC12swift_module10SwiftClass"。
Swift はこの名前をデマングルすることもできます。
echo "_TtC12swift_module10SwiftClass" | swift demangleこれにより、swift_module.SwiftClass が出力されます。
Dart バインディングの生成
#前述のように、例のディレクトリに移動し、ffigen を実行します。
dart run ffigenこれにより、swift_api_bindings.dart が生成されます。
バインディングの使用
#これらのバインディングとの対話は、通常の Objective-C ライブラリの場合とまったく同じです。
import 'dart:ffi';
import 'swift_api_bindings.dart';
void main() {
final lib = SwiftLibrary(DynamicLibrary.open('libswiftapi.dylib'));
final object = SwiftClass.new1(lib);
print(object.sayHello());
print('field = ${object.someField}');
object.someField = 456;
print('field = ${object.someField}');
}モジュール名は生成された Dart API では言及されていないことに注意してください。dylib からクラスをロードするために内部的にのみ使用されます。
これで、次を使用して例を実行できます。
dart run example.dart