package:ffigen を使用した Objective-C および Swift の相互運用
macOS または iOS 上で Dart Native プラットフォーム上で実行される 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 の例
#このガイドでは、package:ffigen
を使用して AVAudioPlayer
のバインディングを生成する 例を説明します。この API には少なくとも macOS SDK 10.7 が必要なので、必要に応じてバージョンを確認して Xcode を更新してください。
$ xcodebuild -showsdks
Objective-C API をラップするバインディングの生成は、C API をラップするのと似ています。API を記述するヘッダーファイルを package:ffigen
に直接指定し、dart:ffi
でライブラリをロードします。
package:ffigen
は LLVM を使用して Objective-C ヘッダーファイルを解析するため、まずそれをインストールする必要があります。詳細については、ffigen README の LLVM のインストールを参照してください。
ffigen の設定
#まず、package:ffigen
を dev 依存関係として追加します。
$ dart pub add --dev ffigen
次に、API を含む Objective-C ヘッダーのバインディングを生成するように ffigen を設定します。ffigen の設定オプションは、トップレベルの ffigen
エントリの下に pubspec.yaml
ファイルに記述します。または、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
ヘッダーです。
設定例を見るとわかるもう 1 つの重要な点は、exclude オプションと include オプションです。デフォルトでは、ffigen
はヘッダーで見つかったすべてのもの、およびそれらのバインディングが他のヘッダーで依存するすべてのもののバインディングを生成します。ほとんどの Objective-C ライブラリは、非常に大きな Apple の内部ライブラリに依存しています。フィルターなしでバインディングが生成されると、結果のファイルは数百万行になる可能性があります。この問題を解決するために、ffigen の設定には、興味のないすべての関数、構造体、enum などをフィルターで除外できるフィールドがあります。この例では、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 が生成されます。
このファイルには、FFI を使用してすべての API 関数をロードし、それらを呼び出すための便利なラッパーメソッドを提供するネイティブライブラリラッパーである AVFAudio
というクラスが含まれています。このファイルの他のクラスはすべて、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());
この例の目的は、コマンドライン引数として指定されたオーディオファイルを1つずつ再生することです。各引数について、まず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.');
}
コールバックとマルチスレッドの制限
#マルチスレッドの問題は、DartのObjective-C相互運用性の実験的サポートにおける最大の制限事項です。これらの制限事項は、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 dispatchドキュメントを参照してください。
最後の点に関して、Dartのアイソレートはスレッドを切り替えることができますが、常に一度に1つのスレッドでのみ実行されます。したがって、対話しているAPIは、必ずしもスレッドセーフである必要はありません。ただし、スレッドに対して敵対的でなく、どのスレッドから呼び出すかについての制約がない限りは問題ありません。
これらの制限事項を念頭に置いていれば、Objective-Cコードを安全に操作できます。
Swift の例
#この例では、SwiftクラスをObjective-Cと互換性を持たせ、ラッパーヘッダーを生成し、Dartコードから呼び出す方法を示しています。
Objective-C ラッパーヘッダーの生成
#Swift APIは、@objc
アノテーションを使用することで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
のみを認識します。したがって、この構成のほとんどは、言語をobjc
に設定することを含め、Objective-Cの例に似ています。
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
マクロ内の文字列は少しわかりにくいですが、モジュール名とクラス名が含まれていることがわかります。"_TtC12
swift_module
10
SwiftClass
"
。
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
特に明記されていない限り、このサイトのドキュメントは Dart 3.5.3 を反映しています。ページの最終更新日は 2024-04-11 です。ソースを表示または問題を報告する。