Skip to content

[pigeon] Separate native interop Dart bindings into a conditionally imported companion file - #13393

Merged
auto-submit[bot] merged 9 commits into
flutter:mainfrom
tarrinneal:pigeon-web-safe-native-interop
Oct 29, 2026
Merged

auto-submit[bot] merged 9 commits into
flutter:mainfrom
tarrinneal:pigeon-web-safe-native-interop

Conversation

@tarrinneal

@tarrinneal tarrinneal commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Separates generated Dart native interop code (dart:ffi, dart:io, package:jni, package:ffi, package:objective_c, and .ffi.dart/.jni.dart bindings) into a companion *.native_interop.g.dart file that is conditionally imported (if (dart.library.ffi)) by the main *.g.dart file.

This allows the main generated *.g.dart file (data classes, enums, codecs, and method-channel APIs) to be imported on Web without failing dart2js / dart2wasm compilation due to transitive dart:ffi / dart:io imports (preventing the issue worked around in #13380).

Pre-Review Checklist

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-assist bot 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

  1. 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

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Oct 22, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 Piinks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 @FlutterApi and 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} {', '}', () {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. ${api.name}Registrar.dartApi in the main file is never read anywhere, so mutating registrar.dartApi after registration (for example setting it to null or swapping implementations) silently has no effect on registered native callbacks, and all the if (dartApi != null) ... else branches in _${api.name}Registrar become unreachable.
  2. On Web, calling registrar.register(api) mutates registrar.dartApi = api before 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!) &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 emits import './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 the pigeon_native import while writeFlutterApi and _writeHostApiNativeInteropInterface still emit pigeon_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}(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two small things on pigeonRegister_${api.name} and pigeonGetInstance_${api.name} since conditional imports require them to be public top-level functions:

  1. disallowedPrefixes reserves pigeon_ (with the underscore), pigeonVar_, pigeonChannelCodec, and capitalized Pigeon, so lowercase pigeonGetInstance_ and pigeonRegister_ aren't reserved. A schema like:
    import 'package:pigeon/pigeon.dart';
    
    class pigeonGetInstance_PingApi {
      pigeonGetInstance_PingApi({this.id});
      int? id;
    }
    
    @HostApi()
    abstract class PingApi {
      void ping(pigeonGetInstance_PingApi payload);
    }
    passes validation and emits duplicate top-level pigeonGetInstance_PingApi declarations in the main file. Using an existing disallowed prefix (such as pigeon_ or PigeonInternal) avoids that.
  2. 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).

@tarrinneal tarrinneal added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 29, 2026
@auto-submit
auto-submit Bot merged commit 3258d45 into flutter:main Oct 29, 2026
13 checks passed
flutter-zl pushed a commit to flutter-zl/flutter that referenced this pull request Oct 30, 2026
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
gmackall pushed a commit to gmackall/flutter that referenced this pull request Nov 3, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autosubmit Merge PR when tree becomes green via auto submit App CICD Run CI/CD p: pigeon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants