Skip to content

Add FFI and JNI support to Swift and Kotlin#11352

Open
tarrinneal wants to merge 107 commits intoflutter:mainfrom
tarrinneal:Kamyshin
Open

Add FFI and JNI support to Swift and Kotlin#11352
tarrinneal wants to merge 107 commits intoflutter:mainfrom
tarrinneal:Kamyshin

Conversation

@tarrinneal
Copy link
Copy Markdown
Contributor

@tarrinneal tarrinneal commented Mar 25, 2026

This PR introduces optional Native Interop to Pigeon, enabling direct communication between Dart and native code without the overhead of traditional MethodChannel serialization. It leverages FFI (Foreign Function Interface) for Swift (iOS/macOS) and JNI (Java Native Interface) for Kotlin (Android).

This represents a significant architectural shift, moving from message-based passing to direct memory sharing and function calls. It also updates the concurrency model for asynchronous methods, moving from completion handlers/callbacks to modern language features: async/await in Swift and Coroutines in Kotlin.

Generators Covered

Swift Generator: Updated to support FFI bindings and async/await for asynchronous methods.
Kotlin Generator: Updated to support JNI bindings and Kotlin Coroutines for asynchronous methods.
Dart Generator: Updated to handle the generated interop bindings on the Dart side.

What's In Scope

  1. Infrastructure: Added core support for useFfi and useJni options.
  2. Automation: Implemented multi-step generation flows that automatically invoke jnigen and ffigen to produce final bindings.
  3. Config Generation: Added jnigen_config_generator.dart and ffigen_config_generator.dart to generate the necessary configuration files for the external tools.
  4. Documentation: Added a detailed native_interop_guide.md explaining prerequisites, setup, and usage.
    Tests: Added ni_tests.dart and associated generated files and integration tests to verify the feature.

What's Out of Scope

  1. Other Languages: This PR specifically targets Swift and Kotlin for the Native Interop feature. Support for Objective-C, C++, and GObject is not included in this interop implementation, and may not be in the future.
  2. Performance Optimization for Complex Classes: As noted in the guide, there is a known performance regression when transferring complex classes with many fields compared to MethodChannel Pigeon. This PR delivers the functional infrastructure, but optimizing this specific case is left for follow-up work.
  3. Non-instant released data. Currently all data that is sent over host or flutter api surfaces is converted to the correct shape and type for the language it is moving toward and the data created in the other language is then discarded. This presents some inefficiencies and potential workflows that are not yet available.

work toward flutter/flutter#182230
design doc flutter/flutter#181430

@tarrinneal tarrinneal added CICD Run CI/CD and removed CICD Run CI/CD labels Mar 31, 2026
@tarrinneal tarrinneal added CICD Run CI/CD and removed CICD Run CI/CD labels Apr 2, 2026
@github-actions github-actions Bot added CICD Run CI/CD and removed CICD Run CI/CD platform-linux platform-windows labels Apr 9, 2026
Copy link
Copy Markdown
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some very early review notes; since it's going to be a long review I thought I should post things incrementally. So far I've mostly just looked at the smaller bits, not the full generators yet (and not the tests).

Please also add a real PR description, with issue references and a high level overview of the PR (what it does, what generators it covers, what's in and out of scope here, etc.)

Comment thread packages/pigeon/example/app/ios/Runner/Messages.g.swift
Comment thread packages/pigeon/example/app/lib/src/event_channel_messages.g.dart
Comment thread packages/pigeon/lib/src/ast.dart
Comment thread packages/pigeon/lib/src/generator_tools.dart
Comment thread packages/pigeon/lib/src/generator_tools.dart Outdated
Comment thread packages/pigeon/lib/src/pigeon_lib.dart Outdated
Comment thread packages/pigeon/lib/src/pigeon_lib.dart Outdated
Comment thread packages/pigeon/lib/src/pigeon_lib.dart
Comment thread packages/pigeon/lib/src/pigeon_lib_internal.dart Outdated
Comment thread packages/pigeon/lib/src/pigeon_lib_internal.dart Outdated
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 16, 2026
Copy link
Copy Markdown
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still working, but here's another incremental comment drop.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should consider changing to two example apps, one that uses method-channel-based Pigeon, and one that use the FFI-based Pigeon. I would expect the common use case to just be one or the other, so putting both in the same app could be confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have the infrastructure to handle two example apps?

Copy link
Copy Markdown
Collaborator

@stuartmorgan-g stuartmorgan-g May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if you have example/thing1/ and example/thing2/ as example apps, our tooling will understand that and do things with both of them.

// #enddocregion config
@HostApi()
abstract class NativeInteropExampleApi {
void doSomething();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not actually seeing any generated code corresponding to this. Is this file unused except for excerpting? If so that's pretty confusing for anyone looking at the example to see how to use this.

Comment thread packages/pigeon/pigeons/ni_tests.dart Outdated
Comment thread packages/pigeon/pigeons/ni_tests.dart Outdated
Comment thread packages/pigeon/pigeons/ni_tests.dart Outdated
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be checked in? If so we should use some kind of naming convention for it to indicate that it's generated if possible.

expect(listEquals(echoObject, list), true);
});

// // Currently need set up
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

override fun noop() {
api?.let {
try {
return api!!.noop()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My Kotlin-fu is not strong, but surely there's a way to do a guard that doesn't require force unwrapping.

return '';
}
typeArgumentString += '>';
return typeArgumentString;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can do final typeArgumentString; at the top, make the internals assignments instead of +=, and then return '<$typeArgumentString>';. That avoids the reassignable variable and the string concatenation.

indent.writeln('}');
} else {
indent.format('''
var sdkPath = '/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk';
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can punt on it for now, but at some point we'll need to figure out how to allow configuring iOS vs macOS vs both as SDK options.

final indent = Indent();
indent.writeln('// ignore_for_file: prefer_const_constructors');
indent.writeln("import 'package:jnigen/jnigen.dart';");
indent.writeln("import 'package:logging/logging.dart';");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we write these configs, we should also parse the pubspec and check that these dependencies exist, and if not either add them automatically, or tell the user they should add them. (Same for ffigen.)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for the runtime dependencies (jni, objective_c)

Copy link
Copy Markdown
Collaborator

@stuartmorgan-g stuartmorgan-g left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried playing with this in local_auth_android, and ran into some issues pretty quickly, although I may have been holding it wrong. I would definitely recommend trying to do an initial conversion of a plugin (that one already uses kotlin generation) to see if things work.

if (generatorOptions.useJni) {
indent.writeln('import androidx.annotation.Keep');
}
indent.writeln('import io.flutter.plugin.common.BasicMessageChannel');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make all of these conditional; they are unused when using JNI.

Comment on lines 254 to 256
if (generatorOptions.package != null && !generatorOptions.useJni) {
indent.writeln('package ${generatorOptions.package}');
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were you including the namepace in the class list in the jnigen config file?

We definitely want to use the package namespace if we are going to avoid a lot of disruption to clients.

Comment thread packages/pigeon/pigeons/ni_tests.dart Outdated
@ConfigurePigeon(
PigeonOptions(
dartOptions: DartOptions(),
kotlinOptions: KotlinOptions(useJni: true),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs say appDirectory is required with JNI, but this isn't using it. And using it didn't work for me, unless I was doing something wrong. What's the right way to configure at the Pigeon level for a standard plugin-with-example?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. appDirectory in the tests is encoded with a default value so isn't required. That's clearly confusing though so I'll add it here. What was the behavior you were seeing? This didn't do anything at all? were the jnigen bindings not showing up? not in the right folder?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC it was trying to find the example in ./ so jnigen failed? I'll have to try again from scratch since I foolishly didn't capture the details.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, don't worry about it until I push up changes, I'm looking into it already.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 28, 2026
@github-actions github-actions Bot removed the CICD Run CI/CD label Apr 28, 2026
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants