Objective-C and Swift interop using package:ffigen
Dart apps running on the Dart Native platform (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's possible to interact with Objective-C APIs using only dart:ffi.
However, doing so requires significant boilerplate,
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.
For more information about using FFIgen, see the FFIgen README and the additional documentation.
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.
Configure FFIgen for Objective-C
#
First, add package:ffigen as a dev dependency and the helpers
package:objective_c and package:ffi as a regular dependency:
$ dart pub add dev:ffigen objective_c ffi
Then, configure FFIgen to generate bindings for the
Objective-C header containing the API.
Configure FFIgen using YAML or Dart code; we recommend Dart for new projects.
The YAML config will be deprecated in future versions of FFIgen.
Start by creating a generate_code.dart script somewhere in your package.
We recommend placing this file in my_package/tool.
The generate_code.dart script should create an FfiGenerator object,
which will contain all your configuration options,
then call its .generate() method.
import 'package:ffigen/ffigen.dart';
final config = FfiGenerator(
);
void main() => config.generate();
First, you'll tell FFIgen where to find the API you're trying to
generate bindings for.
To do this, set the headers.entryPoints option.
For this example, you'll load AVAudioPlayer.h.
This is part of the AVFAudio framework,
which is located in your Xcode installation.
FFIgen includes some helper functions to locate these sorts of APIs,
such as macSdkPath.
Using these helper functions makes your code generation script
more reliable across different machines,
which might have different SDK installation locations.
The macSdkPath utility finds the macOS SDK by running xcrun --show-sdk-path --sdk macosx.
You can run this command in a terminal to locate your macOS SDKs,
or with --sdk iphoneos to find your iOS SDKs.
When generating bindings for an Apple API,
exploring these directories is a great way to find
the right headers to pass to FFIgen.
import 'package:ffigen/ffigen.dart';
final config = FfiGenerator(
headers: Headers(
entryPoints: [
Uri.file(
'$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h',
),
],
),
);
void main() => config.generate();
Next, you'll define the output file.
The main output of FFIgen is a single Dart file
containing bindings for the given inputs.
This file's location is defined by the output.dartFile option.
FFIgen sometimes generates a .m file,
containing Objective-C code required for interop with the API.
FFIgen generates this file only if the API requires it
(for example, if you're using blocks or protocols).
By default, this file has the same name as the Dart bindings,
but with .m at the end of the file name.
You can change its location with the output.objectiveCFile option.
If FFIgen produces this file, you must compile it into your package,
otherwise you might get runtime exceptions relating to missing symbols.
For this example, FFIgen doesn't generate a .m file.
import 'package:ffigen/ffigen.dart';
final config = FfiGenerator(
headers: Headers(
entryPoints: [
Uri.file(
'$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h',
),
],
),
output: Output(
dartFile: Uri.file('avf_audio_bindings.dart'),
),
);
void main() => config.generate();
Finally, tell FFIgen which parts of the input API to generate bindings for.
By default, FFIgen filters out all the bindings.
In this case, to generate bindings for AVAudioPlayer,
which is an Objective-C interface,
you have to set the objectiveC.interfaces field.
Setting the objectiveC field tells FFIgen
to generate bindings for the Objective-C language.
By default, FFIgen generates C bindings.
import 'package:ffigen/ffigen.dart';
final config = FfiGenerator(
headers: Headers(
entryPoints: [
Uri.file(
'$macSdkPath/System/Library/Frameworks/AVFAudio.framework/Headers/AVAudioPlayer.h',
),
],
),
objectiveC: ObjectiveC(
interfaces: Interfaces.includeSet({'AVAudioPlayer'}),
),
output: Output(
dartFile: Uri.file('lib/avf_audio_bindings.dart'),
),
);
void main() => config.generate();
You can use includeMember to filter out specific methods from the class,
and rename or renameMember to rename the included classes or methods.
There are similar options for protocols and categories.
For a full list of configuration options, check out the FFIgen API documentation.
Generate the Objective-C bindings
#
To generate the bindings,
navigate to the example directory and run the script:
$ dart run tool/generate_code.dart
This should generate a large avf_audio_bindings.dart file,
similar to this one.
The main class of interest is AVAudioPlayer.
You might notice other classes in the file
with a comment indicating they are a stub.
FFIgen generates stub bindings for all transitive dependencies
of the directly included APIs.
To generate full bindings for these stubs,
add them to the includes in your config.
This stubbing behavior can be changed
with the includeTransitive options.
Use the Objective-C 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:
import 'dart:ffi';
import 'package:objective_c/objective_c.dart';
import 'avf_audio_bindings.dart';
const _dylibPath =
'/System/Library/Frameworks/AVFAudio.framework/Versions/Current/AVFAudio';
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
}
Because this example loads a system library,
the dylib path points to the framework's internal 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 don't need to load anything.
The example plays 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 toDartString() method that converts it back to a Dart String.
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
for (final file in args) {
final fileStr = NSString(file);
print('Loading $file');
}
}
The audio player expects an NSURL, so next,
use the fileURLWithPath:
method to convert the NSString to an NSURL.
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
for (final file in args) {
final fileStr = NSString(file);
print('Loading $file');
final fileUrl = NSURL.fileURLWithPath(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:
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
for (final file in args) {
final fileStr = NSString(file);
print('Loading $file');
final fileUrl = NSURL.fileURLWithPath(fileStr);
final player = AVAudioPlayer.alloc().initWithContentsOfURL(fileUrl);
if (player == null) {
print('Failed to load audio.');
continue;
}
}
}
This Dart AVAudioPlayer object is a wrapper around an underlying
Objective-C AVAudioPlayer* object pointer.
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 reference is automatically released.
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.
NSTimeInterval is a type alias for double,
so you can immediately use the Dart .ceil() method
to round up to the next second:
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
for (final file in args) {
final fileStr = NSString(file);
print('Loading $file');
final fileUrl = NSURL.fileURLWithPath(fileStr);
final player = AVAudioPlayer.alloc().initWithContentsOfURL(fileUrl);
if (player == null) {
print('Failed to load audio.');
continue;
}
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:
void main(List<String> args) async {
DynamicLibrary.open(_dylibPath);
for (final file in args) {
final fileStr = NSString(file);
print('Loading $file');
final fileUrl = NSURL.fileURLWithPath(fileStr);
final player = AVAudioPlayer.alloc().initWithContentsOfURL(fileUrl);
if (player == null) {
print('Failed to load audio.');
continue;
}
final durationSeconds = player.duration.ceil();
print('$durationSeconds sec');
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 introduces complexity to interop between Objective-C and Dart. This stems from differences between Dart isolates and OS threads, and how Apple's APIs handle concurrency:
- Dart isolates aren't 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 which thread a callback will run on.
- 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 block created in one isolate
might be invoked on a thread running a different isolate,
or no isolate at all.
Depending on the type of block you are using,
this could cause your app to crash.
When a block is created, the isolate it was created in is its owner.
Blocks created using FooBlock.fromFunction
must be invoked on the owner isolate's thread,
otherwise they will crash.
Blocks created using FooBlock.listener or FooBlock.blocking
can be safely invoked from any thread,
and the function they wrap will (eventually) be invoked
inside the owner isolate,
though these constructors are only supported for blocks that return void.
If there is user demand for it, FooBlock.blocking might
add support for non-void return values in the future.
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.
In recent versions of Flutter, the main isolate runs on the platform thread,
so this isn't an issue when invoking these thread-locked APIs
from the main isolate.
If you need to invoke these APIs from other isolates,
or you need to support older versions of Flutter,
you can use the runOnPlatformThread
function.
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. The API you interact with doesn't need to be thread safe, as long as it 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.
The process below is manual. There's an experimental project to automate these steps called Swiftgen.
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.
import Foundation
@objc public class SwiftClass: NSObject {
@objc public func sayHello() -> String {
return "Hello from Swift!";
}
@objc public var someField = 123;
}
To interact with a third-party library you can't modify, 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:
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 of its methods,
make sure they're all annotated with @objc and public.
Configuring FFIgen for Swift
#
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.
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'
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.
A key configuration difference for wrapped Swift APIs
is the objc-interfaces -> module option.
When swiftc compiles the library,
it gives the Objective-C interface a module prefix.
Internally, SwiftClass is 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're 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:
SWIFT_CLASS("_TtC12swift_module10SwiftClass")
@interface SwiftClass : NSObject
The string inside the macro 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 Swift bindings
#As before, to generate the bindings, navigate to the example directory and run FFIgen:
$ dart run ffigen
This generates swift_api_bindings.dart.
Using the Swift bindings
#Interacting with these bindings is exactly the same as for a normal Objective-C library:
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
Unless stated otherwise, the documentation on this site reflects Dart 3.9.2. Page last updated on 2025-11-7. View source or report an issue.