Add FFI and JNI support to Swift and Kotlin#11352
Conversation
stuartmorgan-g
left a comment
There was a problem hiding this comment.
More WIP review comments; still haven't finished the full pass.
There was a problem hiding this comment.
Why do we have two Pigeon files in this example? I would expect just one, using FFI/JNI (and not including Windows or Linux builds).
|
|
||
| | Feature | MethodChannels | Native Interop | | ||
| | :--- | :--- | :--- | | ||
| | **Communication Mechanism** | Asynchronous message passing over platform channels | Direct memory-bound function calls (Dart FFI / JNI) | |
There was a problem hiding this comment.
I don't this row is adding any information that's not in the summary introducing this section, so could be removed.
| | Feature | MethodChannels | Native Interop | | ||
| | :--- | :--- | :--- | | ||
| | **Communication Mechanism** | Asynchronous message passing over platform channels | Direct memory-bound function calls (Dart FFI / JNI) | | ||
| | **Platform Support** | All supported platforms (Android, iOS, macOS, Windows, Linux) | Android, iOS, and macOS only (Windows and Linux not supported) | |
There was a problem hiding this comment.
No need for the parenthetical in the last cell. Anyone interested in Windows or Linux can see it's not in the list.
| | :--- | :--- | :--- | | ||
| | **Communication Mechanism** | Asynchronous message passing over platform channels | Direct memory-bound function calls (Dart FFI / JNI) | | ||
| | **Platform Support** | All supported platforms (Android, iOS, macOS, Windows, Linux) | Android, iOS, and macOS only (Windows and Linux not supported) | | ||
| | **Serialization Overhead** | High (data is serialized/deserialized to/from binary) | Low/Zero (uses direct memory sharing or native references) | |
There was a problem hiding this comment.
This seems misleading; what Pigeon returns isn't shared memory or native references, it's a copy. I think this row should be something like "Overhead | High (serialization and multiple copies) | Low (minimal copying)"
| | **Platform Support** | All supported platforms (Android, iOS, macOS, Windows, Linux) | Android, iOS, and macOS only (Windows and Linux not supported) | | ||
| | **Serialization Overhead** | High (data is serialized/deserialized to/from binary) | Low/Zero (uses direct memory sharing or native references) | | ||
| | **Latency** | Higher (requires message loop scheduling) | Extremely low (direct execution) | | ||
| | **Synchronous Host Calls** | Not supported (always asynchronous) | Fully supported (true synchronous execution) | |
There was a problem hiding this comment.
Neither of the parentheticals is needed here.
|
|
||
| String get fullFfiName { | ||
| if (type.baseName == 'List' || type.baseName == 'Map') { | ||
| return ffiName; |
There was a problem hiding this comment.
Is this supposed to be the same? If so, it seems like some refactoring along the way make this method useless.
| } | ||
|
|
||
| String get primitiveToDartMethodName { | ||
| switch (type.baseName) { |
There was a problem hiding this comment.
As above, consider a switch expression.
| if (type.isClass || type.isEnum) { | ||
| return '$name${_getNullableSymbol(type.isNullable)}.toJni()'; | ||
| } else if (!type.isNullable && | ||
| (type.baseName == 'int' || type.baseName == 'double' || type.baseName == 'bool')) { |
There was a problem hiding this comment.
The int/double/bool baseName triple occurs a number of times in this file; is there a concept you can abstract to a helper method here?
| String getJniCollectionTypeAnnotations({bool isParameter = false, bool isAsynchronous = false}) { | ||
| if (type.baseName == 'List') { | ||
| return '<${getJniCollectionTypes(isParameter: isParameter, isAsynchronous: isAsynchronous)}>'; | ||
| } else if (type.baseName == 'Map') { |
There was a problem hiding this comment.
The if and else if have the same outcome here, so could be consolidated.
| String get jniCollectionTypes => getJniCollectionTypes(); | ||
| } | ||
|
|
||
| String _getNullableSymbol(bool nullable) => nullable ? '?' : ''; |
There was a problem hiding this comment.
_getNullabilitySymbol so it's clearer that it can be non-nullable?
stuartmorgan-g
left a comment
There was a problem hiding this comment.
Another review drop!
I believe I've made it through all the non-test code now, so the end is in sight. (After all, how much test code can there be?)
| } | ||
| } | ||
|
|
||
| ffi_bridge.MessagesPigeonTypedData toPigeonTypedData(TypedData value) { |
There was a problem hiding this comment.
A lot of these helpers aren't file-private; is that intentional? Do we need to make these visible to Pigeon clients?
| } | ||
| } | ||
|
|
||
| String getToDartCall( |
There was a problem hiding this comment.
A lot of these non-trivial helpers could use a Dart doc comment explaining very briefly what they do/are for, and what any non-obvious parameter means.
| return '$name${_getNullableSymbol(type.isNullable)}.toFfi()'; | ||
| } | ||
| if (type.isEnum && !type.isNullable) { | ||
| return 'ffi_bridge.${type.baseName}.values[$name]'; |
There was a problem hiding this comment.
The magic string ffi_bridge appears a lot, and should be a file-level constant instead.
| return 'NSObject${_getNullableSymbol((forceNullable || type.isNullable) && !forceNonNullable)}'; | ||
| } | ||
|
|
||
| return '${ffiName.replaceAll('ffi_bridge.', prefix)}${_getNullableSymbol((forceNullable || type.isNullable) && !forceNonNullable)}'; |
There was a problem hiding this comment.
If sometimes we want the prefix and sometimes we don't, why not have withPrefix as a parameter to ffiName, instead of unconditionally adding it there and then conditionally removing it?
| if (type.baseName == 'List') { | ||
| return '<$ffiCollectionTypes>'; | ||
| } else if (type.baseName == 'Map') { | ||
| return '<$ffiCollectionTypes>'; |
There was a problem hiding this comment.
As above, List and Map are the same and can be collapsed.
| environment: env, | ||
| ); | ||
| if (jnigenResult.exitCode != 0) { | ||
| print(''' |
There was a problem hiding this comment.
I highly recommend printing this second, not first, so it's the most obvious thing someone sees when the command fails.
|
|
||
| String? pubspecPath; | ||
| if (appDirectory != null && appDirectory.isNotEmpty) { | ||
| pubspecPath = findPubspecPath(path.join(appDirectory, 'placeholder.dart')); |
There was a problem hiding this comment.
Let's just adjust findPubspecPath to take a Directory instead of appending placeholders in some cases, and then the places where we start with a Dart path we can call it with File(dartPath).parent
| pubspecPath ??= findPubspecPath(path.join(Directory.current.path, 'placeholder.dart')); | ||
|
|
||
| if (pubspecPath == null) { | ||
| return errors; |
There was a problem hiding this comment.
We want to return no errors if we couldn't find a pubspec? If so, please add a comment here saying why.
| } | ||
| } | ||
| } | ||
| } catch (_) {} |
There was a problem hiding this comment.
Why are we silently dropping all exceptions without returning any errors?
| Error( | ||
| message: | ||
| 'Missing required dependency "$dep" in pubspec.yaml for $apiName native interop support.\n' | ||
| 'Please add "$dep" to your dependencies or dev_dependencies in your pubspec.yaml file.', |
There was a problem hiding this comment.
We should be distinguishing between dev and non-dev dependencies in both the validation and the error message. E.g., if someone adds objective_c as a dev dependency, it shouldn't pass validation. Similarly, if they are completely missing objective_c, as a dependency, they shouldn't have to guess where to add it.
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
Tests: Added ni_tests.dart and associated generated files and integration tests to verify the feature.
What's Out of Scope
work toward flutter/flutter#182230
design doc flutter/flutter#181430