目次

package:ffigen を使用した Objective-C および Swift の相互運用

macOS または iOS 上で Dart Native プラットフォーム上で実行される Dart モバイル、コマンドライン、およびサーバーアプリは、dart:ffipackage: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:ffipackage: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:ffigenLLVM を使用して Objective-C ヘッダーファイルを解析するため、まずそれをインストールする必要があります。詳細については、ffigen README の LLVM のインストールを参照してください。

ffigen の設定

#

まず、package:ffigen を dev 依存関係として追加します。

$ dart pub add --dev ffigen

次に、API を含む Objective-C ヘッダーのバインディングを生成するように ffigen を設定します。ffigen の設定オプションは、トップレベルの ffigen エントリの下に pubspec.yaml ファイルに記述します。または、ffigen の設定を独自の .yaml ファイルに記述することもできます。

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 にのみ興味があるため、他のすべてを除外できます。

yaml
  exclude-all-by-default: true
  objc-interfaces:
    include:
      - 'AVAudioPlayer'

このように AVAudioPlayer が明示的に含まれているため、ffigen は他のすべてのインターフェースを除外します。exclude-all-by-default フラグは、ffigen に他のすべてを除外するように指示します。その結果、AVAudioPlayer とその依存関係 (NSObjectNSString など) を除いて何も含まれません。したがって、数百万行のバインディングではなく、数万行で済みます。

よりきめ細かい制御が必要な場合は、exclude-all-by-default を使用するのではなく、すべての宣言を個別に除外または含めることができます。

yaml
  functions:
    exclude:
      - '.*'
  structs:
    exclude:
      - '.*'
  unions:
    exclude:
      - '.*'
  globals:
    exclude:
      - '.*'
  macros:
    exclude:
      - '.*'
  enums:
    exclude:
      - '.*'
  unnamed-enums:
    exclude:
      - '.*'

これらの exclude エントリはすべて、あらゆるものに一致する正規表現 '.*' を除外します。

また、preamble オプションを使用して、生成されたファイルの先頭にテキストを挿入することもできます。この例では、preamble を使用して、生成されたファイルの先頭にいくつかのリンター無視ルールを挿入しました。

yaml
  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 ライブラリをインスタンス化することです。

dart
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() を使用できます。

dart
  final lib = AVFAudio(DynamicLibrary.process());

この例の目的は、コマンドライン引数として指定されたオーディオファイルを1つずつ再生することです。各引数について、まずDartのStringをObjective-CのNSStringに変換する必要があります。生成されたNSStringラッパーには、この変換を処理する便利なコンストラクターと、DartのStringに戻すtoString()メソッドがあります。

dart
  for (final file in args) {
    final fileStr = NSString(lib, file);
    print('Loading $fileStr');

オーディオプレーヤーはNSURLを期待しているので、次にfileURLWithPath:メソッドを使用して、NSStringNSURLに変換します。Dartのメソッド名では:が無効な文字であるため、バインディングでは_に変換されています。

dart
    final fileUrl = NSURL.fileURLWithPath_(lib, fileStr);

これで、AVAudioPlayerを構築できます。Objective-Cオブジェクトの構築には2つの段階があります。allocはオブジェクトのメモリを割り当てますが、初期化は行いません。init*で始まる名前のメソッドは初期化を行います。一部のインターフェースには、これらの両方のステップを実行するnew*メソッドも用意されています。

AVAudioPlayerを初期化するには、initWithContentsOfURL:error:メソッドを使用します。

dart
    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ラッパーオブジェクトのゲッターとセッターに変換されます。durationreadonlyであるため、ゲッターのみが生成されます。

結果のNSTimeIntervalは単なる型エイリアスのdoubleなので、Dartの.ceil()メソッドをすぐに使用して、次の秒に切り上げることができます。

dart
    final durationSeconds = player.duration.ceil();
    print('$durationSeconds sec');

最後に、playメソッドを使用してオーディオを再生し、ステータスを確認し、オーディオファイルの長さの間待ちます。

dart
    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を拡張するようにしてください。

swift
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も生成します。

生成されたヘッダーを正しく開いて、インターフェイスが期待どおりであることを確認することで、ヘッダーが正しく生成されたことを検証できます。ファイルの最下部には、次のようなものが表示されるはずです。

objc
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の例に似ています。

yaml
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マクロがあります。

objc
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ライブラリの場合とまったく同じです。

dart
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