Direct Native Interop Moves Into Pigeon 29, Giving Flutter a Synchronous FFI/JNI Path
Recently, a netizen happened to discuss this, so let's expand on it. Strictly speaking, Direct Native Interop is indeed not a recent update. The overall timeline is roughly like this:
- 2024: Dart 3.5 began to improve direct interop capabilities for C, Java, Kotlin, Objective-C
- May 2025: Dart 3.8 officially announced the early experience plan for FFIgen and JNIgen's Direct Native Interop
- 2026: The official documentation has integrated the interop methods for C, Java, Objective-C/Swift, and JavaScript into dart.dev/interop, and has begun to continuously improve the related toolchain
Of course, there have been many changes in the past two years, such as:
- The calling path changed: Dart can bypass the traditional Platform Channel message-passing flow and directly call native methods through FFI/JNI
- The code generation method changed: FFIgen and JNIgen can generate Dart Bindings based on native interfaces, so developers no longer need to maintain Kotlin and Swift bridge code themselves
- Flutter's threading architecture has also been adjusted accordingly: the merger of UI thread and Platform thread has been discussed many times
Especially in 2026, there is an additional Pigeon 29. This version officially adds experimental FFI/JNI Direct Native Interop support, which means the implementation of the Native Interop calling system should be about to land at scale.
That is: Dart - generated Binding - JNI / FFI - Kotlin / Swift, this bidirectional path will become the mainstream in the future.
You can now enable it in the Pigeon configuration:
@ConfigurePigeon(
PigeonOptions(
kotlinOptions: KotlinOptions(useJni: true),
swiftOptions: SwiftOptions(
useFfi: true,
ffiModuleName: 'Runner',
),
),
)
Then run dart run pigeon, and the tool will further call jnigen / ffigen to generate the corresponding Kotlin, Swift bridge code and Dart Bindings.
So in fact, Direct Native Interop has existed for a long time, but since September, it has begun to enter Pigeon.
| Comparison | MethodChannel / traditional Pigeon | Direct FFI / JNI |
|---|---|---|
| Calling method | Send message, wait for result | Call generated native binding methods |
| Encoding/Decoding | StandardMessageCodec | Native types, object references, and necessary conversions |
| Flutter Engine message channel | Passes through | Does not pass through |
| Synchronous calls | Asynchronous waiting on Dart side | Supports synchronous calls |
| Thread | Scheduled through message mechanism | Executes on the caller's OS thread |
| Data transfer | Encoding, decoding, possible multiple copies | Reduces copies, but not guaranteed zero-copy |
| Development difficulty | Low, mature ecosystem | More complex binding generation, lifecycle, and build |
| Pigeon direct support | Traditional channels for Android, iOS, macOS, Windows, Linux | Currently Android, iOS, macOS, experimental |
For example, after using JNIgen, you can directly generate Dart Bindings based on Java interfaces. You can then do something like this. This Dart call will go through JNI directly to the Java VM for execution:
import 'example.dart'; // Binding generated by JNIgen
void main() {
print(Example.sum(17, 25)); // 42
}
And FFIgen can turn Objective-C APIs directly into Dart APIs. For example, using AVAudioPlayer as an example, FFIgen reads AVAudioPlayer.h, generates the corresponding Dart Binding, and then you can operate native objects like this:
final fileUrl = NSURL.fileURLWithPath(
NSString('/path/to/audio.mp3'),
);
final player =
AVAudioPlayer.alloc().initWithContentsOfURL(fileUrl);
player?.play();
But Swift is more troublesome than Objective-C, because Dart FFI is oriented to the C ABI, so currently it still needs to connect Swift through an Objective-C compatible interface:
@objc public class SwiftClass: NSObject {
@objc public func sayHello() -> String {
return "Hello from Swift!"
}
}
Also, as mentioned before Dart 3.13, @RecordUse and package:record_use have been added, allowing native FFI Bindings to participate in reachability analysis during AOT compilation.
Previously, Dart-level wrapper functions that were not used could be removed by Tree Shaking, but the Native Library needed to be packaged as a whole. Now, with Link Hook and a native toolchain that supports trimming, it is possible to further remove unused Native Symbols, thereby optimizing the product size.
This is also meaningful for plugins that depend on SQLite, encryption libraries, and audio/video libraries, but it requires corresponding build and link configurations. It's not that any native library will automatically be trimmed after upgrading Dart.
Overall, this feature support is quite good, but it has been delayed for quite a long time.
Top 3 from juejin.cn, machine-translated. The original thread is authoritative.
This Dart call will go directly to the Java VM via JNI. Doesn't that make it even slower... JNI also has overhead.
https://mp.weixin.qq.com/s/igFXsEGjY02akEuv8V11hQ
Impressive