Repository navigation
[pigeon] Separate native interop Dart bindings into a conditionally imported companion file - #13393
Conversation
There was a problem hiding this comment.
Code Review
This pull request separates generated Dart native interop bindings into a conditionally imported companion file (*.native_interop.g.dart) to allow the main generated Dart file to be imported on Web without pulling in dart:ffi or dart:io. It refactors DartGenerator to output this companion file, updates platform checks to use defaultTargetPlatform and kIsWeb, and updates associated tests. A review comment suggests adding a guard clause in generateNativeInterop to prevent a potential null assertion failure if generatorOptions.dartOut is null.
Piinks
left a comment
There was a problem hiding this comment.
Let me know if this tracks, still learning :)
Left a few inline comments on edge cases in the generator, plus a couple of broader notes:
- All the new tests check generated source strings on the VM, while CI already runs Chrome JS and Wasm unit tests on platform_tests/shared_test_plugin_code/test via web_dart_unit_tests.yaml and web_dart_unit_tests_wasm.yaml. Could we add a small unit test in shared_test_plugin_code/test that imports native_interop_tests.gen.dart (for instance calling setUp on a
@FlutterApiand checking that createWithNativeInteropApi throws UnsupportedError when kIsWeb is true)? That would give us real dart2js and dart2wasm compile + runtime coverage on every PR. - Nit/learning - does this fix #13380?
- *native_interop_guide.md and skills/native-interop-migration/SKILL.md list the generated files (.g.dart, .g.jni.dart, .g.ffi.dart, etc.) without mentioning the new .native_interop.g.dart companion file that plugin authors need to commit. Mentioning that in the docs (and noting in CHANGELOG.md that toJni/fromJni and toFfi/fromFfi moved into extensions in the companion file) would also help early adopters regenerating their bindings.
|
|
||
| void _writeNativeInteropEnum(InternalDartOptions generatorOptions, Indent indent, Enum anEnum) { | ||
| indent.newln(); | ||
| indent.writeScoped('extension ${anEnum.name}NativeInterop on ${anEnum.name} {', '}', () { |
There was a problem hiding this comment.
Since _writeNativeInteropFileImports imports the main generated file without a prefix, naming these top-level extensions ${name}NativeInterop can shadow a user-defined class, enum, or API with that name (which isn't blocked by disallowedPrefixes). For example:
import 'package:pigeon/pigeon.dart';
class Foo {
Foo({this.x});
int? x;
}
class FooNativeInterop {
FooNativeInterop({this.y});
String? y;
}
@HostApi()
abstract class FooApi {
FooNativeInterop convert(Foo input, FooNativeInterop existing);
}Validation passes, and in the companion file extension FooNativeInterop on Foo shadows the imported FooNativeInterop class across the whole file, breaking extension FooNativeInteropNativeInterop on FooNativeInterop, FooNativeInterop(...) in fromJni/fromFfi, and the method signatures on _FooApiForNativeInterop. Since nothing outside the companion file references these extensions by name (except sealed class fromJni/fromFfi in the same file), giving the extensions a private or reserved prefix (or prefixing the main-file import) would avoid any collision.
| ); | ||
| } | ||
|
|
||
| final class ${api.name}Registrar { |
There was a problem hiding this comment.
Before this split, ${api.name}Registrar was the actual instance passed to implement(this) / captured by the Objective-C protocol builder closures, which read dartApi on each callback.
Now ${api.name}Registrar still exposes a public mutable dartApi field and sets dartApi = api; here, while pigeonRegister_${api.name} allocates a separate _${api.name}Registrar() in the companion file with its own dartApi:
${api.name}Registrar.dartApiin the main file is never read anywhere, so mutatingregistrar.dartApiafter registration (for example setting it to null or swapping implementations) silently has no effect on registered native callbacks, and all theif (dartApi != null) ... elsebranches in_${api.name}Registrarbecome unreachable.- On Web, calling
registrar.register(api)mutatesregistrar.dartApi = apibefore the stub throws UnsupportedError.
Should ${api.name}Registrar pass itself to pigeonRegister_${api.name} (or drop the public dartApi field if mutating it after registration isn't meant to be supported)?
|
|
||
| const generator = DartGenerator(); | ||
| generator.generate(options.dartOptions!, root, sink, dartPackageName: options.dartPackageName); | ||
| if (usesNativeInterop(options.dartOptions!) && |
There was a problem hiding this comment.
The dartOut check here (dartOut != null && dartOut!.isNotEmpty && dartOut != 'stdout') doesn't line up with writeFileImports (which only checks dartOut != null) or DartGeneratorAdapter.validate:
- When dartOut is
'stdout'(supported by Pigeon for CLI output, and currently allowed by validate with native interop) or'', this skips writing the companion file while writeFileImports still emitsimport './stdout' if (dart.library.ffi) './stdout.native_interop.dart' as pigeon_native;for files that don't exist. - Conversely, if DartGenerator.generate is called with
dartOut: null, writeFileImports omits thepigeon_nativeimport while writeFlutterApi and _writeHostApiNativeInteropInterface still emitpigeon_native.*calls.
Should DartGeneratorAdapter.validate reject dartOut == 'stdout' (and empty dartOut) when native interop is enabled, and keep the pigeon_native import/call emission consistent when dartOut is null?
| dartPackageName: dartPackageName, | ||
| ); | ||
| indent.format(''' | ||
| ${api.name} pigeonRegister_${api.name}( |
There was a problem hiding this comment.
Two small things on pigeonRegister_${api.name} and pigeonGetInstance_${api.name} since conditional imports require them to be public top-level functions:
- disallowedPrefixes reserves
pigeon_(with the underscore),pigeonVar_,pigeonChannelCodec, and capitalizedPigeon, so lowercasepigeonGetInstance_andpigeonRegister_aren't reserved. A schema like:passes validation and emits duplicate top-levelimport 'package:pigeon/pigeon.dart'; class pigeonGetInstance_PingApi { pigeonGetInstance_PingApi({this.id}); int? id; } @HostApi() abstract class PingApi { void ping(pigeonGetInstance_PingApi payload); }
pigeonGetInstance_PingApideclarations in the main file. Using an existing disallowed prefix (such aspigeon_orPigeonInternal) avoids that. - Since plugins may re-export the main generated file (like in [shared_preferences_android] Fix web compilation of apps that import the package #13380), marking these functions
@internal(from package:meta/meta.dart, already imported above) would keep them out of external autocomplete and warn if anyone calls the main-file stubs directly (where they unconditionally throw UnsupportedError even on Android and iOS).
flutter/packages@0b72e92...3742a4e 2026-10-30 [email protected] [pigeon] Allow event channel unregistration (flutter/packages#13438) 2026-10-30 [email protected] [material_ui] Fix Escape key not closing MenuAnchor opened by tap or click (flutter/packages#13430) 2026-10-30 [email protected] manual roll and manual build and copy (flutter/packages#13495) 2026-10-29 [email protected] [pigeon] Separate native interop Dart bindings into a conditionally imported companion file (flutter/packages#13393) 2026-10-29 [email protected] [image_picker_ios] PR 4/5 Migrate the view provider, PHPicker save operation, and image utilities to Swift (flutter/packages#13394) 2026-10-29 [email protected] Sync release-material_ui-1.9.0 to main (flutter/packages#13479) 2026-10-29 [email protected] [material_ui] Unskip web tests that pass on CanvasKit and Skwasm: time_picker_test (flutter/packages#13471) 2026-10-29 [email protected] [vector_graphics] Dispose pictures decoded after their VectorGraphic is removed (flutter/packages#13435) 2026-10-29 [email protected] [camera_android_x] implement CameraX RecordingStats and VideoRecordEvent APIs (flutter/packages#13330) 2026-10-29 [email protected] Sync release-go_router-18.1.2 to main (flutter/packages#13480) 2026-10-29 [email protected] Sync release-cupertino_ui-1.1.5 to main (flutter/packages#13481) 2026-10-29 [email protected] [vector_graphics_compiler] Fix em units in text positions (flutter/packages#13388) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
flutter/packages@0b72e92...3742a4e 2026-10-30 [email protected] [pigeon] Allow event channel unregistration (flutter/packages#13438) 2026-10-30 [email protected] [material_ui] Fix Escape key not closing MenuAnchor opened by tap or click (flutter/packages#13430) 2026-10-30 [email protected] manual roll and manual build and copy (flutter/packages#13495) 2026-10-29 [email protected] [pigeon] Separate native interop Dart bindings into a conditionally imported companion file (flutter/packages#13393) 2026-10-29 [email protected] [image_picker_ios] PR 4/5 Migrate the view provider, PHPicker save operation, and image utilities to Swift (flutter/packages#13394) 2026-10-29 [email protected] Sync release-material_ui-1.9.0 to main (flutter/packages#13479) 2026-10-29 [email protected] [material_ui] Unskip web tests that pass on CanvasKit and Skwasm: time_picker_test (flutter/packages#13471) 2026-10-29 [email protected] [vector_graphics] Dispose pictures decoded after their VectorGraphic is removed (flutter/packages#13435) 2026-10-29 [email protected] [camera_android_x] implement CameraX RecordingStats and VideoRecordEvent APIs (flutter/packages#13330) 2026-10-29 [email protected] Sync release-go_router-18.1.2 to main (flutter/packages#13480) 2026-10-29 [email protected] Sync release-cupertino_ui-1.1.5 to main (flutter/packages#13481) 2026-10-29 [email protected] [vector_graphics_compiler] Fix em units in text positions (flutter/packages#13388) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages-flutter-autoroll Please CC [email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
Separates generated Dart native interop code (
dart:ffi,dart:io,package:jni,package:ffi,package:objective_c, and.ffi.dart/.jni.dartbindings) into a companion*.native_interop.g.dartfile that is conditionally imported (if (dart.library.ffi)) by the main*.g.dartfile.This allows the main generated
*.g.dartfile (data classes, enums, codecs, and method-channel APIs) to be imported on Web without failingdart2js/dart2wasmcompilation due to transitivedart:ffi/dart:ioimports (preventing the issue worked around in #13380).Pre-Review Checklist
[shared_preferences]///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.Footnotes
Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. ↩ ↩2