Skip to content

[pigeon] Fix standalone Native Interop @FlutterApi issues - #13609

Merged
auto-submit[bot] merged 3 commits into
flutter:mainfrom
tarrinneal:ni-flutter-api-fixes
Nov 9, 2026
Merged

auto-submit[bot] merged 3 commits into
flutter:mainfrom
tarrinneal:ni-flutter-api-fixes

Conversation

@tarrinneal

@tarrinneal tarrinneal commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Split out from #13576 per review. These are non-breaking fixes that can land on 29.x ahead of the 30.0.0 multi-engine changes.

  • [dart] Prefixes the internal _<Api>Registrar members (dartApi, register) with pigeonVar_. This means @FlutterApi methods named register or dartApi no longer collide with the JNIgen interface when useJni is enabled, so those names are no longer reserved.
  • [dart] <FlutterApi>.implement now defaults name to the default instance name and treats an empty name as the default. Previously it registered under '', which lookups never matched.
  • Pins objective_c to <9.6.3 in the test packages and the native interop example app, to work around a debug-mode object validity check bug on the iOS simulator ([objective_c] Fix isValidObject bug on iOS simulator dart-lang/native#3788).

work towards flutter/flutter#195877

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

- Prefix internal _<Api>Registrar members with pigeonVar_ so FlutterApi
  methods named register or dartApi no longer collide with JNI.
- Make <Api>.implement fall back to the default instance name when name
  is empty.
@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Nov 9, 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 updates Pigeon to version 29.1.2. It resolves name collisions in the generated Dart code for @FlutterApi when using JNI by prefixing internal registrar members with varNamePrefix, removing dartApi and register from the reserved names list. It also ensures that @FlutterApi implement registrations default to defaultInstanceName instead of an empty string. Additionally, the objective_c dependency is restricted to <9.6.3 across several packages due to an iOS simulator issue. There are no review comments, and I have no feedback to provide.

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

LGTM

@tarrinneal tarrinneal added the autosubmit Merge PR when tree becomes green via auto submit App label Nov 9, 2026
@auto-submit
auto-submit Bot merged commit 13be8f6 into flutter:main Nov 9, 2026
12 checks passed
rmacnak-google pushed a commit to rmacnak-google/flutter that referenced this pull request Nov 10, 2026
…r#196296)

flutter/packages@565c657...08faf64

2026-11-10 [email protected] [pigeon] Pass data classes and
collections in a single buffer per native interop call
(flutter/packages#13265)
2026-11-10 [email protected] [ci] Fix release workflows
(flutter/packages#13611)
2026-11-09 [email protected] [pigeon] Fix standalone Native Interop
@FlutterApi issues (flutter/packages#13609)
2026-11-09 [email protected]
[cross_file_web] Adds support to instantiate the web PlatformXFile
without needing to import web apis (flutter/packages#13395)
2026-11-09 [email protected] Sync release-go_router-18.1.4 to
main (flutter/packages#13606)
2026-11-09 [email protected] Sync release-material_ui-1.11.0 to
main (flutter/packages#13605)
2026-11-09 [email protected] [vector_graphics_compiler] Resolve Flutter
artifact cache via package:cli_util (flutter/packages#13252)
2026-11-09 [email protected] [ci] Remove `pull_request_target`
workflows (flutter/packages#13581)
2026-11-09 [email protected] Roll Flutter from
acade0f to d463821 (1 revision) (flutter/packages#13601)

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
auto-submit Bot pushed a commit that referenced this pull request Nov 10, 2026
…e-aware unregistration (#13576)

Associates Native Interop (`useJni` / `useFfi`) `@HostApi` and `@FlutterApi` registrations with the Flutter engine that made them. Lookups route to the calling engine's registration when the engine is known, and detaching one engine removes only that engine's registration.

Previously, attaching a secondary `FlutterEngine` overwrote any existing registration under the same `name`. Detaching that engine via `register(null)` then cleared the entry for all remaining engines in the process. For example, `shared_preferences_android` failed in the main engine after a background engine detached (flutter/flutter#194920).

> [!NOTE]
> Per review, the standalone `@FlutterApi` fixes (the `pigeonVar_` prefix for internal `_<Api>Registrar` members so `register` / `dartApi` no longer collide on JNI, `implement` with an empty `name`, and the `objective_c <9.6.3` test pin) are split out into #13609 so they can land on 29.x first. This PR contains only the unregistration and multi-engine changes, which depend on each other.

### Engine identity without deprecated APIs or new stable requirements

`register` / `unregister` now take the engine handle that plugins and apps already have. Pigeon derives an engine key from it, so moving to `engineId` once flutter/flutter#195881 reaches `stable` is an internal change, not another breaking change:

| Caller | Engine key |
|---|---|
| Kotlin app: `MyApiRegistrar().register(api, flutterEngine)` | `FlutterEngine.engineId` (public on stable; exact) |
| Kotlin plugin: `MyApiRegistrar().register(api, binding)` | `binding.getEngineId()` when the embedding provides a non-zero value (#195881, detected at runtime, survives R8). Otherwise a per-engine key from `binding.binaryMessenger`. |
| Swift: `MyApiSetup.register(api:registrar:)` | `registrar.engineIdentifier` when available and non-zero (detected at runtime). Otherwise a per-engine key from the registrar's messenger. |

Generated code never calls deprecated embedding APIs such as `FlutterPluginBinding.getFlutterEngine()`. An engine ID of `0` is treated as unknown, because real IDs are never `0`. `0` shows up for registrars whose engine is already deallocating, and for unstubbed test doubles, and must not be shared between engines.

Dart lookups (`createWithNativeInteropApi()` / `<HostApi>ForNativeInterop.getInstance()`) choose a registration in this order:
1. The one whose key matches `PlatformDispatcher.instance.engineId`.
2. Otherwise, if the isolate has no engine ID, one from the engine this isolate first resolved a handle from. This is tracked per generated Pigeon file.
3. Otherwise, the most recent registration.

Native `@FlutterApi` lookups use the same keys through `<FlutterApi>Registrar` `getInstance(binding | flutterEngine)` in Kotlin and `getInstance(registrar:)` in Swift.

**Exact vs. best-effort routing.** Routing is exact whenever native code knows the engine identifier: Kotlin `register(api, flutterEngine)` always, and plugin bindings / registrars on Flutter versions that expose it. Otherwise it is best-effort. It is correct when each engine's Dart resolves its first handle from each Pigeon file at startup, but it can pick another engine's registration when:
- Engines are created back-to-back before their Dart starts.
- A file's first lookup happens lazily while another engine is attached.
- Lookups come from `Isolate.spawn` / `Isolate.run` isolates.

macOS plugins receive no detach callback, so macOS registrations currently outlive their engine. The README and guides document these limits and recommend platform channels / `@ProxyApi` for engine-stateful APIs in those cases. Full engine routing remains tracked in flutter/flutter#195877.

### Summary of Changes

* **Engine-keyed registration stacks & `unregister`**: Each `name` keeps an ordered stack of `(engine key, instance)` entries.
  * Re-registering the same instance for the same engine moves it to the top and keeps its existing wrapper.
  * `unregister(api, engine)` removes only that engine's entry, even out of order. If the engine key can't be resolved, it falls back to matching by instance identity, but only when a single engine holds that instance.
* **Detached `@HostApi` safety**: Calls through a handle whose instance was unregistered throw `unregistered-instance` (Kotlin/Swift) instead of crashing. Swift `Setup` wrappers use `resolveApi() throws` instead of `api!`.
* **Engine teardown safety (Swift)**: Engine keys are cached per registrar. `unregister` from `detachFromEngine(for:)` therefore works while the engine is deallocating and its messenger is already gone.
* **Registry thread safety**: Native Interop calls from background isolates run on worker threads. Registration maps, wrapper API references, and engine-key caches are therefore guarded internally:
  * Kotlin uses `synchronized` / `@Synchronized` and `@Volatile`.
  * Swift uses `NSLock` / `NSRecursiveLock`. Removed entries are released after unlocking, so their `deinit` doesn't run while the lock is held.
  * This doesn't make registration a thread-safe API contract. Registration is still expected on the main thread.
* **Dart `@FlutterApi`**:
  * `<FlutterApi>.setUp(null)` deregisters only the current isolate's native callback.
  * Registering via `setUp(a)` or the registrar, then calling `setUp(null)`, now correctly removes `a`.
  * Adds `<FlutterApi>Registrar.unregister`.
  * Registrations are tagged with the isolate's engine so native lookups reach the right isolate.
* **Documentation**:
  * Updates the README, Native Interop guide, migration guide, migration skill, and `CHANGELOG.md`, including the routing guarantees and limits above.
  * Notes that `registrar.publish(instance)` is required for iOS to call `detachFromEngine(for:)`.
* **Tests**:
  * Generator unit tests for Dart, Kotlin, and Swift.
  * Kotlin `NativeInteropRegistrarTest`, iOS `RunnerTests`, and macOS `BasicCompileTest`. These cover:
    * the same API on two engines
    * out-of-order unregister
    * the identity fallback
    * resolve order
    * `@FlutterApi` lookup by engine
  * Real secondary-engine and multi-isolate integration tests.
  * An integration test asserting that registrations use the exact `PlatformDispatcher.instance.engineId` when the platform exposes it. It runs in minified Android release builds to cover R8.

### Breaking Native Interop API Changes (`30.0.0`)

* **Engine-aware registration**:
  * `@HostApi` `register` / `unregister` take the engine: `MyApiRegistrar().register(api, binding)` / `register(api, flutterEngine)` in Kotlin, and `MyApiSetup.register(api:registrar:)` in Swift. `unregister` follows the same shape.
  * Native `@FlutterApi` lookup takes the engine: `getInstance(binding | flutterEngine)` in Kotlin and `getInstance(registrar:)` in Swift. The name-only native `getInstance` is removed.
* **Explicit deregistration**:
  * Replaces `register(null)` / `register(api: nil)` / `registerInstance(null | nil)` with `unregister` / `unregisterInstance`, which remove only the given instance for the given engine.
  * `register` / `registerInstance` require a non-null API and return `void` / `Unit` / `Void`.
* **Swift `@HostApi` `AnyObject` constraint**: FFI Swift `@HostApi` protocols are constrained to `AnyObject` so instances can be matched by identity.
* **Reserved names**: `pigeonResolveInstance`, `pigeonEngineKey`, and `pigeonEngineKeyOf` are reserved as `@HostApi` method names.

Part of flutter/flutter#195877
Part of flutter/flutter#194920

## Pre-Review Checklist

**Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). 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.

[^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.
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.

2 participants