跪拜 Guibai
← All articles
Frontend · Flutter · Android

Direct Native Interop Moves Into Pigeon 29, Giving Flutter a Synchronous FFI/JNI Path

By 恋猫de小郭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Flutter plugin authors stand to lose the hand-written Kotlin and Swift bridge code in favor of generated bindings, gaining synchronous calls and reduced data copying. Pigeon 29's experimental support indicates the Flutter team is preparing to make direct interop the default for new plugins, which will force existing platform-channel-based plugins to rethink their threading and build setup.

Summary

Direct Native Interop has been evolving for two years, and with Pigeon 29 it finally lands inside Flutter's standard plugin toolchain. Dart 3.5 started improving C, Java, Kotlin, and Objective-C interop in 2024; Dart 3.8 announced FFIgen and JNIgen's early access in May 2025; now dart.dev/interop consolidates the C, Java, Objective-C/Swift, and JavaScript paths. The immediate effect: developers can generate Dart bindings from native interfaces and call them synchronously, bypassing the Platform Channel's message encoding and decoding.

Pigeon 29 adds experimental FFI/JNI support. Turning it on requires `useJni: true` for Kotlin and `useFfi: true` for Swift in the Pigeon configuration; `dart run pigeon` then invokes `jnigen`/`ffigen` to produce both bridge code and Dart bindings. The comparison table makes the trade-off clear: direct FFI/JNI runs on the caller's OS thread, supports sync calls, and reduces data copying, but code generation, lifecycle, and build configuration become more complex. Current direct support covers Android, iOS, and macOS, with Windows and Linux still on the traditional channel.

Swift remains the awkward case. Dart FFI targets the C ABI, so Swift classes still need an `@objc` shim to be callable. Separately, Dart 3.13 brought `@RecordUse` and `package:record_use`, letting native FFI bindings participate in AOT reachability analysis, so unused native symbols can be stripped from the final binary when combined with link hooks and a trimming-capable toolchain. That optimization only kicks in with proper build and link setup; simply upgrading Dart won't shrink every plugin.

Takeaways
— Dart's Direct Native Interop lets FFI/JNI calls run synchronously on the caller's OS thread, while Platform Channel calls go through a message loop and wait asynchronously.
— FFIgen and JNIgen generate Dart bindings from native interfaces, eliminating hand-maintained Kotlin and Swift bridge code.
— Pigeon 29 adds experimental FFI/JNI support; enabling it requires `useJni: true` and `useFfi: true` in the Pigeon options, and `dart run pigeon` then invokes `jnigen`/`ffigen`.
— Direct FFI/JNI reduces data copies but does not guarantee zero-copy; it avoids StandardMessageCodec encoding and decoding.
— Swift interop still needs an `@objc` shim because Dart FFI targets the C ABI, so pure Swift APIs aren't directly accessible.
— Dart 3.13's `@RecordUse` lets native FFI bindings participate in AOT reachability analysis, enabling tree shaking of unused native symbols only when the build uses link hooks and a trimming-capable toolchain.
— As of Pigeon 29, direct interop support is experimental for Android, iOS, and macOS; Windows and Linux continue to use traditional Platform Channel.
Conclusions

The two-year timeline shows the Flutter team deliberately kept Direct Native Interop behind separate tooling until Pigeon 29, which is when it enters the standard plugin-generation workflow. That gap is why most developers still see the feature as new.

The trade-off table puts lifecycle and build complexity on the platform-channel side: synchronous calls and fewer copies arrive with the burden of managing generated bindings and native memory, which the old message-codec approach hid.

Swift's dependence on an `@objc` shim is a pragmatic concession to the C ABI, but it means pure Swift projects won't get first-class interop without an extra compatibility layer, slowing adoption in newer iOS codebases.

Concepts & terms
Direct Native Interop
Dart's ability to call native C, Java, Kotlin, or Objective-C/Swift APIs directly through FFI or JNI, bypassing Flutter's Platform Channel message-passing system.
FFIgen
A code generator that reads native C or Objective-C headers and produces Dart bindings for FFI calls, so Dart can invoke native functions.
JNIgen
A code generator that reads Java or Kotlin interfaces and produces Dart bindings that let Dart call into the Java VM via JNI.
Pigeon
Flutter's tool for generating platform-channel code; version 29 adds experimental support for generating FFI/JNI-based bindings instead of message-based codecs.
@RecordUse
A Dart annotation that marks a native binding as used during AOT compilation, enabling tree shaking of unused native symbols when combined with a supporting native toolchain.
From the discussion
Featured comments
zoharSoul

This Dart call will go directly to the Java VM via JNI. Doesn't that make it even slower... JNI also has overhead.

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗