Contents

Objective-C and Swift interop using package:ffigen

Dart mobile, command-line, and server apps running on the Dart Native platform, on macOS or iOS, can use dart:ffi and package:ffigen to call Objective-C and Swift APIs.

dart:ffi enables Dart code to interact with native C APIs. Objective-C is based on and compatible with C, so it is possible to interact with Objective-C APIs using only dart:ffi. However, doing so involves a lot of boilerplate code, so you can use package:ffigen to automatically generate the Dart FFI bindings for a given Objective-C API. To learn more about FFI and interfacing with C code directly, see the C interop guide.

You can generate Objective-C headers for Swift APIs, enabling dart:ffi and package:ffigen to interact with Swift.

Objective-C Example

#

This guide walks you through an example that uses package:ffigen to generate bindings for AVAudioPlayer. This API requires at least macOS SDK 10.7, so check your version and update Xcode if necessary:

$ xcodebuild -showsdks

Generating bindings to wrap an Objective-C API is similar to wrapping a C API. Direct package:ffigen at the header file that describes the API, and then load the library with dart:ffi.

package:ffigen parses Objective-C header files using LLVM, so you'll need to install that first. See Installing LLVM from the ffigen README for more details.

Configuring ffigen

#

First, add package:ffigen as a dev dependency:

$ dart pub add --dev ffigen

Then, configure ffigen to generate bindings for the Objective-C header containing the API. The ffigen configuration options go in your pubspec.yaml file, under a top-level ffigen entry. Alternatively, you can put the ffigen config in its own .yaml file.

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'

The name is the name of the native library wrapper class that will be generated, and the description will be used in the documentation for that class. The output is the path of the Dart file that ffigen will create. The entry point is the header file containing the API. In this example, it is the internal AVAudioPlayer.h header.

Another import thing you'll see, if you look at the example config, is the exclude and include options. By default, ffigen generates bindings for everything it finds in the header, and everything that those bindings depend on in other headers. Most Objective-C libraries depend on Apple's internal libraries, which are very large. If bindings are generated without any filters, the resulting file can be millions of lines long. To solve this problem, the ffigen config has fields that enable you to filter out all the functions, structs, enums, etc., that you're not interested in. For this example, we're only interested in AVAudioPlayer, so you can exclude everything else:

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

Since AVAudioPlayer is explicitly included like this, ffigen excludes all other interfaces. The exclude-all-by-default flag tells ffigen to exclude everything else. The result is that nothing is included except AVAudioPlayer, and its dependencies, such as NSObject and NSString. So instead of several million lines of bindings, you end up with tens of thousands.

If you need more granular control, you can exclude or include all declarations individually, rather than using exclude-all-by-default:

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

These exclude entries all exclude the regular expression '.*', which matches anything.

You can also use the preamble option to insert text at the top of the generated file. In this example, the preamble was used to insert some linter ignore rules at the top of the generated file:

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

See the ffigen readme for a full list of configuration options.

Generating the Dart bindings

#

To generate the bindings, navigate to the example directory, and run ffigen:

$ dart run ffigen

This will search in the pubspec.yaml file for a top-level ffigen entry. If you chose to put the ffigen config in a separate file, use the --config option and specify that file:

$ dart run ffigen --config my_ffigen_config.yaml

For this example, this will generate avf_audio_bindings.dart.

This file contains a class called AVFAudio, which is the native library wrapper that loads all the API functions using FFI, and provides convenient wrapper methods to call them. The other classes in this file are all Dart wrappers around the Objective-C interfaces that we need, such as AVAudioPlayer and its dependencies.

Using the bindings

#

Now you're ready to load and interact with the generated library. The example app, play_audio.dart, loads and plays audio files passed as command line arguments. The first step is to load the dylib and instantiate the native AVFAudio library:

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));

Since you're loading an internal library, the dylib path is pointing at an internal framework dylib. You can also load your own .dylib file, or if the library is statically linked into your app (often the case on iOS) you can use DynamicLibrary.process():

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

The goal of the example is to play each of the audio files specified as command line arguments one by one. For each argument, you first have to convert the Dart String to an Objective-C NSString. The generated NSString wrapper has a convenient constructor that handles this conversion, and a toString() method that converts it back to a Dart String.

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

The audio player expects an NSURL, so next we use the fileURLWithPath: method to convert the NSString to an NSURL. Since : is not a valid character in a Dart method name, it has been translated to _ in the bindings.

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

Now, you can construct the AVAudioPlayer. Constructing an Objective-C object has two stages. alloc allocates the memory for the object, but doesn't initialize it. Methods with names starting with init* do the initialization. Some interfaces also provide new* methods that do both of these steps.

To initialize the AVAudioPlayer, use the initWithContentsOfURL:error: method:

dart
    final player =
        AVAudioPlayer.alloc(lib).initWithContentsOfURL_error_(fileUrl, nullptr);

Objective-C uses reference counting for memory management (through retain, release, and other functions), but on the Dart side memory management is handled automatically. The Dart wrapper object retains a reference to the Objective-C object, and when the Dart object is garbage collected, the generated code automatically releases that reference using a NativeFinalizer.

Next, look up the length of the audio file, which you'll need later to wait for the audio to finish. The duration is a @property(readonly). Objective-C properties are translated into getters and setters on the generated Dart wrapper object. Since duration is readonly, only the getter is generated.

The resulting NSTimeInterval is just a type aliased double, so you can immediately use the Dart .ceil() method to round up to the next second:

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

Finally, you can use the play method to play the audio, then check the status, and wait for the duration of the audio file:

dart
    final status = player.play();
    if (status) {
      print('Playing...');
      await Future<void>.delayed(Duration(seconds: durationSeconds));
    } else {
      print('Failed to play audio.');
    }

Callbacks and multithreading limitations

#

Multithreading issues are the biggest limitation of Dart's experimental support for Objective-C interop. These limitations are due to the relationship between Dart isolates and OS threads, and the way Apple's APIs handle multithreading:

  • Dart isolates are not the same thing as threads. Isolates run on threads, but aren't guaranteed to run on any particular thread, and the VM might change which thread an isolate is running on without warning. There is an open feature request to enable isolates to be pinned to specific threads.
  • While ffigen supports converting Dart functions to Objective-C blocks, most Apple APIs don't make any guarantees about on which thread a callback will run.
  • Most APIs that involve UI interaction can only be called on the main thread, also called the platform thread in Flutter.
  • Many Apple APIs are not thread safe.

The first two points mean that a callback created in one isolate might be invoked on a thread running a different isolate, or no isolate at all. Depending on the type of callback you are using, this could cause your app to crash. Callbacks created using Pointer.fromFunction or NativeCallable.isolateLocal must be invoked on the owner isolate's thread, otherwise they will crash. Callbacks created using NativeCallable.listener can be safely invoked from any thread.

The third point means that directly calling some Apple APIs using the generated Dart bindings might be thread unsafe. This could crash your app, or cause other unpredictable behavior. You can work around this limitation by writing some Objective-C code that dispatches your call to the main thread. For more information, see the Objective-C dispatch documentation.

Regarding the last point, although Dart isolates can switch threads, they only ever run on one thread at a time. So, the API you are interacting with doesn't necessarily have to be thread safe, as long as it is not thread hostile, and doesn't have constraints about which thread it's called from.

You can safely interact with Objective-C code, as long as you keep these limitations in mind.

Swift example

#

This example demonstrates how to make a Swift class compatible with Objective-C, generate a wrapper header, and invoke it from Dart code.

Generating the Objective-C wrapper header

#

Swift APIs can be made compatible with Objective-C, by using the @objc annotation. Make sure to make any classes or methods you want to use public, and have your classes extend NSObject.

swift
import Foundation

@objc public class SwiftClass: NSObject {
  @objc public func sayHello() -> String {
    return "Hello from Swift!";
  }

  @objc public var someField = 123;
}

If you're trying to interact with a third-party library, and can't modify their code, you might need to write an Objective-C compatible wrapper class that exposes the methods you want to use.

For more information about Objective-C / Swift interoperability, see the Swift documentation.

Once you've made your class compatible, you can generate an Objective-C wrapper header. You can do this using Xcode, or using the Swift command-line compiler, swiftc. This example uses the command line:

$ swiftc -c swift_api.swift             \
    -module-name swift_module           \
    -emit-objc-header-path swift_api.h  \
    -emit-library -o libswiftapi.dylib

This command compiles the Swift file, swift_api.swift, and generates a wrapper header, swift_api.h. It also generates the dylib you're going to load later, libswiftapi.dylib.

You can verify that the header generated correctly by opening it, and checking that the interfaces are what you expect. Towards the bottom of the file, you should see something like the following:

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

If the interface is missing, or doesn't have all its methods, make sure they're all annotated with @objc and public.

Configuring ffigen

#

Ffigen only sees the Objective-C wrapper header, swift_api.h. So most of this config looks similar to the Objective-C example, including setting the language to objc.

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

As before, set the language to objc, and the entry point to the header; exclude everything by default, and explicitly include the interface you are binding.

One important difference between the config for a wrapped Swift API and a pure Objective-C API: the objc-interfaces -> module option. When swiftc compiles the library, it gives the Objective-C interface a module prefix. Internally, SwiftClass is actually registered as swift_module.SwiftClass. You need to tell ffigen about this prefix, so it loads the correct class from the dylib.

Not every class gets this prefix. For example, NSString and NSObject won't get a module prefix, because they are internal classes. This is why the module option maps from class name to module prefix. You can also use regular expressions to match multiple class names at once.

The module prefix is whatever you passed to swiftc in the -module-name flag. In this example, it's swift_module. If you don't explicitly set this flag, it defaults to the name of the Swift file.

If you aren't sure what the module name is, you can also check the generated Objective-C header. Above the @interface, you'll find a SWIFT_CLASS macro:

objc
SWIFT_CLASS("_TtC12swift_module10SwiftClass")
@interface SwiftClass : NSObject

The string inside the macro is a bit cryptic, but you can see it contains the module name and the class name: "_TtC12swift_module10SwiftClass".

Swift can even demangle this name for us:

$ echo "_TtC12swift_module10SwiftClass" | swift demangle

This outputs swift_module.SwiftClass.

Generating the Dart bindings

#

As before, navigate to the example directory, and run ffigen:

$ dart run ffigen

This generates swift_api_bindings.dart.

Using the bindings

#

Interacting with these bindings is exactly the same as for a normal Objective-C library:

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}');
}

Note that the module name is not mentioned in the generated Dart API. It's only used internally, to load the class from the dylib.

Now you can run the example using:

$ dart run example.dart