使用 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 類似。將 package:ffigen
直接指向描述 API 的標頭檔,然後使用 dart:ffi
載入函式庫。
package:ffigen
使用 LLVM 解析 Objective-C 標頭檔,因此您需要先安裝它。如需更多詳細資訊,請參閱 ffigen README 中的安裝 LLVM。
設定 ffigen
#首先,新增 package:ffigen
作為開發相依性
$ dart pub add --dev ffigen
然後,設定 ffigen 為包含 API 的 Objective-C 標頭產生繫結。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
標頭。
如果您查看範例設定,您會看到的另一個重要事項是排除和包含選項。預設情況下,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
用於在產生的檔案頂端插入一些 linter 忽略規則。
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 函式的原生函式庫包裝函式,並提供方便的包裝函式方法來呼叫它們。此檔案中的其他類別都是我們需要的 Objective-C 介面的 Dart 包裝函式,例如 AVAudioPlayer
及其相依性。
使用繫結
#現在您已準備好載入並與產生的函式庫互動。範例應用程式 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 路徑會指向內部 framework dylib。您也可以載入自己的 .dylib
檔案,或者如果函式庫是靜態連結到您的應用程式中(iOS 上通常如此),則可以使用 DynamicLibrary.process()
。
final lib = AVFAudio(DynamicLibrary.process());
這個範例的目標是依序播放以命令列引數指定的多個音訊檔案。對於每個引數,您首先必須將 Dart 的 String
轉換為 Objective-C 的 NSString
。產生的 NSString
包裝器有一個方便的建構函式來處理此轉換,以及一個 toString()
方法將其轉換回 Dart 的 String
。
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 物件有兩個階段。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 包裝器物件上的 getter 和 setter。由於 duration
是 readonly
,因此僅會產生 getter。
產生的 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 isolates 和作業系統執行緒之間的關係,以及 Apple API 處理多執行緒的方式所造成。
- Dart isolates 與執行緒不是同一件事。Isolates 在執行緒上執行,但不保證會在任何特定執行緒上執行,並且 VM 可能會在沒有警告的情況下變更 isolate 正在執行的執行緒。有一個 開放的功能要求,可讓 isolates 固定在特定的執行緒上。
- 雖然
ffigen
支援將 Dart 函式轉換為 Objective-C blocks,但大多數 Apple API 不保證回呼會在哪個執行緒上執行。 - 大多數涉及 UI 互動的 API 只能在主執行緒上呼叫,在 Flutter 中也稱為平台執行緒。
- 許多 Apple API 並非執行緒安全。
前兩點表示在一個 isolate 中建立的回呼可能會在執行不同 isolate 的執行緒上呼叫,或者根本沒有任何 isolate。根據您使用的回呼類型,這可能會導致您的應用程式崩潰。使用 Pointer.fromFunction
或 NativeCallable.isolateLocal
建立的回呼必須在擁有者 isolate 的執行緒上呼叫,否則它們會崩潰。使用 NativeCallable.listener
建立的回呼可以安全地從任何執行緒呼叫。
第三點表示直接使用產生的 Dart 綁定呼叫某些 Apple API 可能會不安全。這可能會導致您的應用程式崩潰,或導致其他無法預測的行為。您可以透過編寫一些將呼叫分派到主執行緒的 Objective-C 程式碼來解決此限制。如需詳細資訊,請參閱 Objective-C dispatch 文件。
關於最後一點,雖然 Dart isolates 可以切換執行緒,但它們一次只會在一個執行緒上執行。因此,您正在互動的 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 的設定之間的一個重要區別: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.6.0。頁面最後更新於 2024-04-11。 檢視原始碼 或 回報問題。